Compare commits

3 Commits
dev ... master

Author SHA1 Message Date
root
7eadb4c49c updating to latest 2021-11-04 01:18:18 -04:00
root
f92b773514 update 9.14.21 2021-09-14 23:33:09 -04:00
root
4cf44009a2 moved compoletely to node-red for automations. Cleaned up a bit. Upgraded to latest version of HA 2020-09-21 12:17:37 -04:00
179 changed files with 21645 additions and 714 deletions

1
.HA_VERSION Normal file
View File

@@ -0,0 +1 @@
2021.10.5

327
.storage/auth Normal file
View File

@@ -0,0 +1,327 @@
{
"version": 1,
"key": "auth",
"data": {
"users": [
{
"id": "7e128bb4681b4f9680cff80d395082c8",
"group_ids": [
"system-admin"
],
"is_owner": false,
"is_active": true,
"name": "Supervisor",
"system_generated": true
},
{
"id": "e2ac2db8725247b0bb84b03718a01c39",
"group_ids": [
"system-admin"
],
"is_owner": true,
"is_active": true,
"name": "Dan",
"system_generated": false
}
],
"groups": [
{
"id": "system-admin",
"name": "Administrators"
},
{
"id": "system-users",
"name": "Users"
},
{
"id": "system-read-only",
"name": "Read Only"
}
],
"credentials": [
{
"id": "940968b2abe54eda8d33a50fa40637bf",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"auth_provider_type": "homeassistant",
"auth_provider_id": null,
"data": {
"username": "dan"
}
}
],
"refresh_tokens": [
{
"id": "6c2c924255cd43e082bbd9ec0d13f160",
"user_id": "7e128bb4681b4f9680cff80d395082c8",
"client_id": null,
"client_name": null,
"client_icon": null,
"token_type": "system",
"created_at": "2020-05-29T17:56:30.594677+00:00",
"access_token_expiration": 1800.0,
"token": "ad8e000af3c797b8d1b40671c13211d88f4bec1e28d66bbd0110648c3e1f5d89ae473e6683c914dbee11008067de6fea0acebed9e7e98ea4d04274f343b4c693",
"jwt_key": "2bebb9fbbecd2a42d24db8f53bab9a493faee41eb64a90ff1140cb849e0e9737f6e50323d4967049262e80a723b50c10af7b487b4d7858dca075adb70e8a8e52",
"last_used_at": "2021-11-04T05:13:50.720694+00:00",
"last_used_ip": "172.30.32.2",
"credential_id": null,
"version": null
},
{
"id": "30bed7edcb3b483b9c258fc19e6b67e6",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "http://192.168.86.40:8123/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-05-29T17:57:53.352192+00:00",
"access_token_expiration": 1800.0,
"token": "9f2e16a48be4c4e20cb8b59ef06cbd0fbea461ad532f9f3e8c4822142778d027f442481d780ec2ac1dfa8a793c6ad8aaffa65a2ce785bb6b966cebdc9507281c",
"jwt_key": "0d48d3ef9b5a750a51e587f123b6d22661c7dd97fb57d9f39766a4c6777369adce1c078bf6b813c8608bdb412eee4ceec9ef1952198e1147bde29580574e895c",
"last_used_at": "2020-08-27T15:45:56.542944+00:00",
"last_used_ip": "192.168.86.199",
"credential_id": null,
"version": null
},
{
"id": "fcfe376305564c99bbda3ba26c03f579",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-05-29T19:10:57.949284+00:00",
"access_token_expiration": 1800.0,
"token": "6a1bb9dd79d3bc02a40730a50dd1688dbd37532d7821b941cada99f81e880cccc5d659165b00018e3a205ed9434b318cc56c14ebd14a4c35c5c87a0edb2635eb",
"jwt_key": "0ad684830d6643bed4cd41d1d41e96f1102362d821dbc13dddbd65a54bf95e7f05881214b27eaf7316dc561f8f097fe78511dcfe65f52fe232ec3c15ed52e5b8",
"last_used_at": "2020-07-20T12:18:33.184240+00:00",
"last_used_ip": "192.168.86.198",
"credential_id": null,
"version": null
},
{
"id": "46c858b4e420475ebc54b3d1a9d58dd2",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": null,
"client_name": "nodered",
"client_icon": null,
"token_type": "long_lived_access_token",
"created_at": "2020-05-29T19:20:52.141510+00:00",
"access_token_expiration": 315360000.0,
"token": "5c0355f71cb377e769ef10bd182b8ff06b5e29ece64cb40335f8d4ffea9915286306c6e9bf51fe52b6aa04989d0763970b5675c5b2a1d92070f8c0e2dd33380a",
"jwt_key": "4b49e05c4c91a0b7e8a51f0bc8cb6f531afc798139cb8c1f8aa354463d762475c891853cdfae5227ec3b2c285fee20e059ea7400497803b86d5766f17d91c8ae",
"last_used_at": "2020-05-29T19:20:52.142065+00:00",
"last_used_ip": null,
"credential_id": null,
"version": null
},
{
"id": "19575d9287324e3489345fe9db5a4936",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-05-29T21:32:22.614204+00:00",
"access_token_expiration": 1800.0,
"token": "08a756075d2f15addae8983a725011dd532bd7b57750d11ebb8b37c8feeeb153028e84ed73494ecf1b721ef39ceb9d2836221fb6566a5f53c4e8164c930a20ac",
"jwt_key": "45e9b816dfd5b419729ff97fb90fb7602c171b623062f6554693a4142de719f60adf59c195f3c6002faa5903924bc5e2f263f07bf67bd2801526655456082f11",
"last_used_at": "2021-11-04T05:14:27.221933+00:00",
"last_used_ip": "192.168.86.1",
"credential_id": null,
"version": null
},
{
"id": "b5eeca33991941a8a41030ca2c6d349e",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "http://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-05-29T21:44:35.781505+00:00",
"access_token_expiration": 1800.0,
"token": "ee1c89a76a734f28857b02a4450bcedbd38ac077b6b8e758c9f6622618513b1a6c6334ca0c3d52682cfc01a1514ee94ca0be05f2f927ac2e95b43e2b3fe98ec8",
"jwt_key": "7b35033548dd9eb840ac0eb6134a90ee16bbe05625c7a0d76d25e5b9dfae358e5ea4022d571879dca848676b101216c147452de5ddb08a189753e39321b24e98",
"last_used_at": "2021-06-11T14:37:40.221368+00:00",
"last_used_ip": "192.168.86.198",
"credential_id": null,
"version": null
},
{
"id": "1c1b24887db846099d6a7f5ff867f70b",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://home-assistant.io/android",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-05-30T00:48:00.660451+00:00",
"access_token_expiration": 1800.0,
"token": "d26165ea00a1e93ef83d2b41edee7816ddae5677af7d532876ff5cd90fd4641686c942b96cc4b894ad7194d673cacd25e8612069d208b85867e2c6fc2181e967",
"jwt_key": "46f60304aab09ba84a79173449142a15c031247be8a205abd45541c073163b22e429efa460ff5f50b0b256c3c760bdbb0238cbf2ae5194a7bd6184aeb287b900",
"last_used_at": "2021-11-04T02:54:24.337524+00:00",
"last_used_ip": "192.168.86.36",
"credential_id": null,
"version": null
},
{
"id": "df8c4c4bef054d49b81233979d289f63",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-05-30T00:52:06.132796+00:00",
"access_token_expiration": 1800.0,
"token": "d6490c1d9a076075d66b78d985793454449c86da2f94e701c08dc6b3f28430f8b551ad94bf983ecdd34ebaa5667b781a6a487b03c3c03add69f04832b8529099",
"jwt_key": "74eea0472bf0a9ae8de80d91dc6a749a68849a4ad14c50f3d81a69923cc5e5d2d67e248414c771b85c5859a194812197d902037bea7b80db8f7702dfb4fb7970",
"last_used_at": "2020-08-26T15:15:40.903869+00:00",
"last_used_ip": "192.168.86.198",
"credential_id": null,
"version": null
},
{
"id": "343a96be8a4546aa927d46fe6af00fd0",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-07-21T15:49:59.553351+00:00",
"access_token_expiration": 1800.0,
"token": "0e06da7483669a4bc6e2e258af50f56f243529782ffe796f2d09886d28ed388ad04ecd17e1786dfabf770e919107742b8cab0e9f8a76bcc325c511fcc7db6cf5",
"jwt_key": "61232b195e40bd7ccec9c2d819045da50148e438043e101d75174c6317bada7df8e456914a0b5f0b98124e5caf99052f21a38e0e35807d308466701c0e157d4b",
"last_used_at": "2021-05-23T18:20:23.167244+00:00",
"last_used_ip": "192.168.86.198",
"credential_id": null,
"version": null
},
{
"id": "041349f25ed44541a85639cc1f08a937",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "http://192.168.86.40:8123/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2020-08-26T15:22:39.788527+00:00",
"access_token_expiration": 1800.0,
"token": "48998af2f96ac72599e4b2de1b74a44ad1606282b407b5101b0259ba8578d86dbaf3129ec86c26b215539e87d0f4c77ae8ff1eb8c3b6f7df16f5c24afc7325b1",
"jwt_key": "2c0e67d51d64856009d6c86736cce01fc0d1ba17b7dd1b0f6c45ccbafce527fecb922261484351fd3d7ca4fee51b4d0aeb12c5c46503e010d96dec6364c9d0de",
"last_used_at": "2020-09-01T05:47:09.903831+00:00",
"last_used_ip": "192.168.86.163",
"credential_id": null,
"version": null
},
{
"id": "0583c68546c5471399811e7776ef743b",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2021-05-30T11:48:34.952165+00:00",
"access_token_expiration": 1800.0,
"token": "265ef95c0a386a84b200cf2bfe70f5712feec53e9864d87f335bccb2c3d5db6db6747ce76bef0cd6cc7a009d07b09022bc3fe280d96399a16ce64f519f4d8bf2",
"jwt_key": "2c55003fa049504ead0f3079b26984fd31f2c48213631d752538a341209b823060fe367ddb8f5fd0e92dc22251e98cbed147c60a51a7a8b36bd7cd5a4f36dcb3",
"last_used_at": "2021-10-27T14:24:21.227406+00:00",
"last_used_ip": "192.168.86.1",
"credential_id": "940968b2abe54eda8d33a50fa40637bf",
"version": "2021.5.5"
},
{
"id": "696e4e819d2646d5b3f26db96c4b249c",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "http://192.168.86.40:8123/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2021-08-24T12:50:08.936245+00:00",
"access_token_expiration": 1800.0,
"token": "b569881512898fb71e8443b7641c83d5e562834c74c804d405b86b7886afaf456a7c0f2cb4b1525f1a5d5816fd9ab786c1371563db934fd36479176fcc1e8aa3",
"jwt_key": "c24b749ff1a7c9e9108310bb1c8cc283cce287cbf8130f73a5871c90d70d20a17a0a656d525068c2e54d1dca0799164d2741a3c0ae39c0071e1379a644b5c189",
"last_used_at": "2021-08-31T15:13:14.867288+00:00",
"last_used_ip": "192.168.86.28",
"credential_id": "940968b2abe54eda8d33a50fa40637bf",
"version": "2021.8.8"
},
{
"id": "47ab2ae9cbd8424f81195a3d12680039",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "http://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2021-09-25T02:52:33.244560+00:00",
"access_token_expiration": 1800.0,
"token": "cd06129e6dfa54e8db8a5f6559478011fb1b931d2c448ce697b30c5335d41d827d80d61188e5704b22a7357f15cc474dcf9340c21f0a865a468c855f61f6d08a",
"jwt_key": "5fed4b07949f2354dfb112eb4f7614d89468c5a7dd7b1e0bae174f1421e5d2ebf2048f7086c95945ed06c197b23dfdce6c9e92bf8765d484c48acf757c8d6677",
"last_used_at": "2021-09-25T03:28:09.039643+00:00",
"last_used_ip": "192.168.86.1",
"credential_id": "940968b2abe54eda8d33a50fa40637bf",
"version": "2021.9.6"
},
{
"id": "cd93d6c34ec042de8e5a16ee8d10ae95",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": null,
"client_name": "alexa",
"client_icon": null,
"token_type": "long_lived_access_token",
"created_at": "2021-10-08T18:34:55.438178+00:00",
"access_token_expiration": 315360000.0,
"token": "833793e994ef8167e6ee95fa7a71a8120885738cf919bdf0ba6e15ce42e3ed4cc2b7d558ffb86ca5117a8cc1d3cd979f143b92bdbe2e6e3805793564c144a8f2",
"jwt_key": "d8ddd1cb3f712bdc25b8c9d14a9a8320e6c6ce31c9efb81b11a7b80d73e0261b11fed98ac69361668366b489f005de15943c9538d46ee13a6307910f4c1e0f3b",
"last_used_at": "2021-10-08T18:34:55.439116+00:00",
"last_used_ip": null,
"credential_id": null,
"version": "2021.9.6"
},
{
"id": "9f434eebc801473cb2cca812dfc7f962",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2021-10-18T19:10:21.306301+00:00",
"access_token_expiration": 1800.0,
"token": "ad4e7579362846c8a8c35be76b6a3cf0262796c02e1caa6046be8349fe6679dc197a6f1ec463bebdc6037179e8703af791945af32a5b78714817424e2fb9dfbe",
"jwt_key": "9d401dd56ee3571d5672bf67dc380a4f9e1ad5fd0affe44657f2e2fcc7f0ce81bb440dcd26b475bedd5fe85ea11351187f1602c4dd8a6a21f85e75238561758a",
"last_used_at": "2021-10-18T19:10:21.307226+00:00",
"last_used_ip": "192.168.86.1",
"credential_id": "940968b2abe54eda8d33a50fa40637bf",
"version": "2021.10.5"
},
{
"id": "efe7250858bf4a1e8108715613f8be2f",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "http://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2021-11-03T18:45:47.481806+00:00",
"access_token_expiration": 1800.0,
"token": "e79efde17ced310ce046722abf625e495ab8919d2fbfbe5f6bc38341ae7bc4bbc76222bd1413dff0bbe9511732dd356663982519c9a55828343f9d0b5a29177d",
"jwt_key": "decab182d5d7aaef7de8b179ae96b939266ddafd3c381a14c7d0c13f7dbbcd14617e3df02503ee52641df361fe7a77fb4ae8a8b9c068cbbafa3fb0f1bc0c4f50",
"last_used_at": "2021-11-03T19:55:15.923429+00:00",
"last_used_ip": "192.168.86.1",
"credential_id": "940968b2abe54eda8d33a50fa40637bf",
"version": "2021.10.5"
},
{
"id": "4773ad9c3d2842619b71b173ce104c1f",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"client_id": "https://house.dandembinski.com/",
"client_name": null,
"client_icon": null,
"token_type": "normal",
"created_at": "2021-11-03T18:46:57.611006+00:00",
"access_token_expiration": 1800.0,
"token": "8821cc18a22a74045b8ad6264f52c2afcbaebe4a50a857fe210c66231bf442148c307956e4d10d3694f71d7f045093243abb31ca1e73d1fa630dc4a63ddad9ae",
"jwt_key": "607f92e59edff2c44eb7896ee2e96d03608127bd7c0d07c0be7241ac8be05ae4422de5967bb39336fbaef0c65524471aca9352a083533b37121876e2b55e2a72",
"last_used_at": "2021-11-04T03:44:57.901906+00:00",
"last_used_ip": "192.168.86.1",
"credential_id": "940968b2abe54eda8d33a50fa40637bf",
"version": "2021.10.5"
}
]
}
}

View File

@@ -0,0 +1,12 @@
{
"data": {
"users": [
{
"password": "JDJiJDEyJC41dU1CRHNNM3A2akMuSFVQNVB1YXViaVFCT3dPV2UuTmdxWDl5R0xOOVpqby5lY3RPVVhL",
"username": "dan"
}
]
},
"key": "auth_provider.homeassistant",
"version": 1
}

View File

@@ -0,0 +1,36 @@
{
"version": 1,
"key": "core.area_registry",
"data": {
"areas": [
{
"name": "Kitchen",
"id": "ac918f0c0e314c3e9cd18b486842bb24"
},
{
"name": "Bedroom",
"id": "8e5446424de1458db58dda26c21a5285"
},
{
"name": "Office",
"id": "cccf16afc4f5422898e671517a120ed6"
},
{
"name": "Entryway",
"id": "03ff2a472caf42b7bd88ce96b7325862"
},
{
"name": "Living",
"id": "living"
},
{
"name": "Basement",
"id": "basement"
},
{
"name": "Dinning Room",
"id": "dinning_room"
}
]
}
}

14
.storage/core.config Normal file
View File

@@ -0,0 +1,14 @@
{
"data": {
"elevation": 206,
"external_url": "https://house.dandembinski.com",
"internal_url": "http://192.168.86.40:8123/",
"latitude": 41.39887176326016,
"location_name": "Home",
"longitude": -82.23310858011247,
"time_zone": "America/New_York",
"unit_system": "imperial"
},
"key": "core.config",
"version": 1
}

View File

@@ -0,0 +1,415 @@
{
"version": 1,
"key": "core.config_entries",
"data": {
"entries": [
{
"entry_id": "d449b87b7a8a4de2a1ee7ad105ac30b1",
"version": 1,
"domain": "met",
"title": "Home",
"data": {
"track_home": true
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "onboarding",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "77d68408d0a44c3c8ffa3747452065f1",
"version": 1,
"domain": "hue",
"title": "Philips hue",
"data": {
"host": "192.168.86.44",
"username": "84TbphY7UA59qtwukn1sx7vn3LX98WffZSy5n-rg"
},
"options": {
"allow_hue_groups": false,
"allow_unreachable": true
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "import",
"unique_id": "00178812ec3b",
"disabled_by": null
},
{
"entry_id": "487ba0d979a943a6b590f834f80258d0",
"version": 1,
"domain": "mqtt",
"title": "192.168.86.198",
"data": {
"broker": "192.168.86.198",
"port": 1888,
"password": "1234",
"username": "hassio",
"birth_message": {
"topic": "homeassistant/status",
"payload": "online",
"qos": 0,
"retain": false
},
"will_message": {
"topic": "homeassistant/status",
"payload": "offline",
"qos": 0,
"retain": false
},
"discovery": true
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "4034b3eddaef4bee93ba6eb68f6d9e5e",
"version": 1,
"domain": "mobile_app",
"title": "SM-N960U",
"data": {
"app_data": {
"push_url": "https://mobile-apps.home-assistant.io/api/sendPush/android/v1",
"push_token": "fyPvng4VdH4:APA91bF403pSt2EUPcwwAJ4eSLHwCMWXjn0Q4-KJf8whlEqJGC7VRGmujrQhUu64rngMhFvYo8rR7VrYW20k84Tt-6bG2AymdjI90pXwYOb_9uaxThOCzl8GfGcSKGCzB0oxmqWKmFtB"
},
"app_id": "io.homeassistant.companion.android",
"app_name": "Home Assistant",
"app_version": "2021.10.0-full (761)",
"device_id": "98756dd6f94f600d",
"device_name": "SM-N960U",
"manufacturer": "samsung",
"model": "SM-N960U",
"os_name": "Android",
"os_version": "29",
"supports_encryption": false,
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "registration",
"unique_id": "io.homeassistant.companion.android-98756dd6f94f600d",
"disabled_by": null
},
{
"entry_id": "52efee6cb13d42f3a5a13a89bd813400",
"version": 1,
"domain": "owntracks",
"title": "OwnTracks",
"data": {
"cloudhook": false,
"secret": "3b54858961193ac9527c65ec53817048",
"webhook_id": "3fce02123ef2e6efe569acbb7a1144839645e96c496eb5594dcde83f46b0e5d9"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "24c023984771440bb36266e23675c31a",
"version": 1,
"domain": "roku",
"title": "Ignored",
"data": {
"host": "192.168.86.30"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "YR00AV370849",
"disabled_by": null
},
{
"entry_id": "8595c96fda7a4a2eaa4e9c7434236f57",
"version": 1,
"domain": "samsungtv",
"title": "Ignored",
"data": {
"id": "7f8d7dec-32da-4314-9391-cb1acc922b95",
"manufacturer": "Samsung Electronics",
"model": "UN55J6200"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "192.168.86.42",
"disabled_by": null
},
{
"entry_id": "7edb292b13634d9c9ef43ff79665efd3",
"version": 1,
"domain": "brother",
"title": "Ignored",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "u63883m5n395203",
"disabled_by": null
},
{
"entry_id": "94d2d69d16ba44d3a5fb345f3d2944cb",
"version": 1,
"domain": "cast",
"title": "Ignored",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "cast",
"disabled_by": null
},
{
"entry_id": "de42e81841a94b7783ba796bba7deaa6",
"version": 1,
"domain": "ipp",
"title": "Ignored",
"data": {
"host": "192.168.86.32",
"name": "Brother HL-L2360D series"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "e3248000-80ce-11db-8000-30055ca5f565",
"disabled_by": null
},
{
"entry_id": "41d744a4348146db8d6f8cd07a36e1b7",
"version": 1,
"domain": "upnp",
"title": "Ignored",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "uuid:3f422f86-082a-4aec-9740-e71645c0fb9d::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
"disabled_by": null
},
{
"entry_id": "babd9cc14c314f6db8c5ce578dfa9564",
"version": 3,
"domain": "zha",
"title": "HubZ Smart Home Controller, s/n: 90F00055 - Silicon Labs",
"data": {
"device": {
"baudrate": 57600,
"path": "/dev/serial/by-id/usb-Silicon_Labs_HubZ_Smart_Home_Controller_90F00055-if01-port0"
},
"radio_type": "ezsp"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "69831ce2912c4cccbd488b95302d08b7",
"version": 1,
"domain": "upnp",
"title": "Ignored",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "uuid:45ed9cb0-ec89-40f9-9ab6-cbcef376e442::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
"disabled_by": null
},
{
"entry_id": "8f3041c2c7134aef825c276d667c92a3",
"version": 1,
"domain": "upnp",
"title": "Ignored",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "uuid:ab6abbcf-b79f-49dc-be9a-1ff32d48463a::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
"disabled_by": null
},
{
"entry_id": "796149ee9cd94b5d8297bb0f78096a3f",
"version": 1,
"domain": "homekit_controller",
"title": "Ignored",
"data": {
"AccessoryIP": "192.168.86.34",
"AccessoryPort": 34127
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "30:0b:8b:2b:f8:2c",
"disabled_by": null
},
{
"entry_id": "6f0422dffc2011eab962e33c52698c7d",
"version": 1,
"domain": "sonarr",
"title": "192.168.86.198",
"data": {
"base_path": "/api",
"port": 8989,
"ssl": false,
"verify_ssl": false,
"host": "192.168.86.198",
"api_key": "0db45f76dbfc432ab5c86d8346d5165e"
},
"options": {
"upcoming_days": 1,
"wanted_max_items": 50
},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "966199e4041f11eb892b391fb72793f3",
"version": 1,
"domain": "ecobee",
"title": "ecobee",
"data": {
"api_key": "8DSRpsILZJNUIJXvSzY6C4ufrMQNQsHa",
"refresh_token": "5bQ1gz8GSwHc3XIyTlM8qKkDjqb715Qr"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "user",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "5b411d516be8bea4ccc31c46e6a36c0c",
"version": 1,
"domain": "hassio",
"title": "Supervisor",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "system",
"unique_id": "hassio",
"disabled_by": null
},
{
"entry_id": "50aa91eb44e7c92d2b4a8a0ba028351e",
"version": 2,
"domain": "samsungtv",
"title": "Living room (UN55J6200)",
"data": {
"host": "192.168.86.42",
"mac": "78:bd:bc:79:42:85"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "7f8d7dec-32da-4314-9391-cb1acc922b95",
"disabled_by": null
},
{
"entry_id": "4701417a1902ab7be569765641579e22",
"version": 1,
"domain": "upnp",
"title": "OnHub",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "uuid:29422ada-a94b-4a3d-bcc9-aa24e2ac5c1e::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
"disabled_by": null
},
{
"entry_id": "107d2d027a46461e1852309411b07b24",
"version": 1,
"domain": "homekit_controller",
"title": "Roku 4630X 5DE3",
"data": {
"AccessoryIP": "192.168.86.30",
"AccessoryPort": 55290
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "9d:11:93:91:d3:c7",
"disabled_by": null
},
{
"entry_id": "168e1b19293508dafb169cd8270442a4",
"version": 1,
"domain": "zwave_js",
"title": "Z-Wave JS",
"data": {
"url": "ws://core-zwave-js:3000",
"usb_path": "/dev/serial/by-id/usb-Silicon_Labs_HubZ_Smart_Home_Controller_90F00055-if00-port0",
"network_key": "1C07307043F183F395CECE7572FAA2B7",
"use_addon": true,
"integration_created_addon": false,
"s2_access_control_key": "65C8447AAFEF6C433E86C2B7209F6CD6",
"s2_authenticated_key": "AD3DF19206BC91EA5FB2D3B910E18E86",
"s2_unauthenticated_key": "4E8DF41AD19402953C04B7086A08035C"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "hassio",
"unique_id": 4189927064,
"disabled_by": null
},
{
"entry_id": "58119655eea8bc6c0f5a9ac217b73372",
"version": 1,
"domain": "spotify",
"title": "Spotify",
"data": {},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": null,
"disabled_by": null
},
{
"entry_id": "737ca8cab48eb7350044f5ddf9742f3c",
"version": 1,
"domain": "upnp",
"title": "OnHub",
"data": {
"hostname": "192.168.86.1"
},
"options": {},
"pref_disable_new_entities": false,
"pref_disable_polling": false,
"source": "ignore",
"unique_id": "uuid:107841be-833f-4bda-83e7-bbb4922d5fae::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
"disabled_by": null
}
]
}
}

View File

@@ -0,0 +1,725 @@
{
"version": 1,
"key": "core.device_registry",
"data": {
"devices": [
{
"config_entries": [
"77d68408d0a44c3c8ffa3747452065f1"
],
"connections": [
[
"mac",
"00:17:88:12:ec:3b"
]
],
"identifiers": [
[
"hue",
"001788FFFE12EC3B"
]
],
"manufacturer": "Signify",
"model": "BSB001",
"name": "Philips hue",
"sw_version": "01043155",
"entry_type": null,
"id": "1f01297a279d458eba50b1a7fced66e1",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"77d68408d0a44c3c8ffa3747452065f1"
],
"connections": [],
"identifiers": [
[
"hue",
"00:00:00:00:00:43:9a:80"
]
],
"manufacturer": "Philips",
"model": "ZGPSWITCH",
"name": "Hue Tap 1",
"sw_version": null,
"entry_type": null,
"id": "be9781e5f1c34bd496e75ecac9d1dcc6",
"via_device_id": "1f01297a279d458eba50b1a7fced66e1",
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"77d68408d0a44c3c8ffa3747452065f1"
],
"connections": [],
"identifiers": [
[
"hue",
"00:17:88:01:00:d8:5c:9c-0b"
]
],
"manufacturer": "Philips",
"model": "LCT001",
"name": "Lamp left",
"sw_version": "5.127.1.26581",
"entry_type": null,
"id": "9d17b8ce7d7c40eb9046f414524209cb",
"via_device_id": "1f01297a279d458eba50b1a7fced66e1",
"area_id": "living",
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"77d68408d0a44c3c8ffa3747452065f1"
],
"connections": [],
"identifiers": [
[
"hue",
"00:17:88:01:00:fa:c0:cd-0b"
]
],
"manufacturer": "Philips",
"model": "LCT001",
"name": "Table",
"sw_version": "5.127.1.26581",
"entry_type": null,
"id": "93d1f0f9d47146a7bec94a5514fe6173",
"via_device_id": "1f01297a279d458eba50b1a7fced66e1",
"area_id": "living",
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"77d68408d0a44c3c8ffa3747452065f1"
],
"connections": [],
"identifiers": [
[
"hue",
"00:17:88:01:00:ef:e3:c3-0b"
]
],
"manufacturer": "Philips",
"model": "LCT001",
"name": "Lamp right",
"sw_version": "5.127.1.26581",
"entry_type": null,
"id": "e0b8f163c2354efb96ea5d9f2f05fad2",
"via_device_id": "1f01297a279d458eba50b1a7fced66e1",
"area_id": "living",
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"4034b3eddaef4bee93ba6eb68f6d9e5e"
],
"connections": [],
"identifiers": [
[
"mobile_app",
"98756dd6f94f600d"
]
],
"manufacturer": "samsung",
"model": "SM-N960U",
"name": "SM-N960U",
"sw_version": "29",
"entry_type": null,
"id": "e65bc44886b84792ba48ae70e4fa57d7",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"52efee6cb13d42f3a5a13a89bd813400"
],
"connections": [],
"identifiers": [
[
"owntracks",
"dan_dansphone"
]
],
"manufacturer": null,
"model": null,
"name": null,
"sw_version": null,
"entry_type": null,
"id": "b4bf3f88d4924c94a93ff59c144c47c6",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"00:0d:6f:00:0a:ff:73:f8"
]
],
"identifiers": [
[
"zha",
"00:0d:6f:00:0a:ff:73:f8"
]
],
"manufacturer": "ZHA",
"model": "EZSP = Silicon Labs EmberZNet protocol: Elelabs, HUSBZB-1, Telegesis",
"name": "Zigbee Coordinator",
"sw_version": null,
"entry_type": null,
"id": "f05eae618c3d4f908b9022a05e88ce95",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"b0:ce:18:14:03:0b:3e:1d"
]
],
"identifiers": [
[
"zha",
"b0:ce:18:14:03:0b:3e:1d"
]
],
"manufacturer": "sengled",
"model": "E11-G13",
"name": "sengled E11-G13",
"sw_version": "0x00000003",
"entry_type": null,
"id": "9ea4a15795c44e8db70f13f26cca9a93",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"b0:ce:18:14:03:0b:3e:fd"
]
],
"identifiers": [
[
"zha",
"b0:ce:18:14:03:0b:3e:fd"
]
],
"manufacturer": "sengled",
"model": "E11-G13",
"name": "sengled E11-G13",
"sw_version": "0x00000003",
"entry_type": null,
"id": "c57a16aa70724d0191fdb72f346d2dbb",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"b0:ce:18:14:03:0b:22:ba"
]
],
"identifiers": [
[
"zha",
"b0:ce:18:14:03:0b:22:ba"
]
],
"manufacturer": "sengled",
"model": "E11-G13",
"name": "sengled E11-G13",
"sw_version": "0x00000003",
"entry_type": null,
"id": "2b0f187abe74477fab0d035f8817a8f6",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"b0:ce:18:14:03:14:68:d6"
]
],
"identifiers": [
[
"zha",
"b0:ce:18:14:03:14:68:d6"
]
],
"manufacturer": "sengled",
"model": "E11-G13",
"name": "sengled E11-G13",
"sw_version": "0x00000009",
"entry_type": null,
"id": "1e92b72b37af42708165f56b484d1990",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"b0:ce:18:14:03:0b:40:af"
]
],
"identifiers": [
[
"zha",
"b0:ce:18:14:03:0b:40:af"
]
],
"manufacturer": "sengled",
"model": "E11-G13",
"name": "sengled E11-G13",
"sw_version": "0x00000003",
"entry_type": null,
"id": "1828828e7283407894093611afecfa76",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"6f0422dffc2011eab962e33c52698c7d"
],
"connections": [],
"identifiers": [
[
"sonarr",
"6f0422dffc2011eab962e33c52698c7d"
]
],
"manufacturer": "Sonarr",
"model": null,
"name": "Activity Sensor",
"sw_version": "3.0.6.1342",
"entry_type": "service",
"id": "6f365493fc2011ea9f59eb7dc9b07531",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"966199e4041f11eb892b391fb72793f3"
],
"connections": [],
"identifiers": [
[
"ecobee",
"411928765012"
]
],
"manufacturer": "ecobee",
"model": "ecobee3 lite Smart Thermostat",
"name": "Home",
"sw_version": null,
"entry_type": null,
"id": "97300b68041f11ebacaa396522618ab4",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"d449b87b7a8a4de2a1ee7ad105ac30b1"
],
"connections": [],
"identifiers": [
[
"met"
]
],
"manufacturer": "Met.no",
"model": "Forecast",
"name": "Forecast",
"sw_version": null,
"entry_type": "service",
"id": "aabcb64f5f211c38784f45faa5029cea",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"core_ssh"
]
],
"manufacturer": "Official add-ons",
"model": "Home Assistant Add-on",
"name": "Terminal & SSH",
"sw_version": "9.2.1",
"entry_type": "service",
"id": "cd25a61cedb94647fd69655d947cab82",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"core_git_pull"
]
],
"manufacturer": "Official add-ons",
"model": "Home Assistant Add-on",
"name": "Git pull",
"sw_version": "7.12",
"entry_type": "service",
"id": "a3ec358d220baf426ed6c78cc15e543c",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"OS"
]
],
"manufacturer": "Home Assistant",
"model": "Home Assistant Operating System",
"name": "Home Assistant Operating System",
"sw_version": "6.5",
"entry_type": "service",
"id": "5dbbb7f52f9bcacdd90ffa48bf809110",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"00:12:4b:00:22:ea:ee:33"
]
],
"identifiers": [
[
"zha",
"00:12:4b:00:22:ea:ee:33"
]
],
"manufacturer": "eWeLink",
"model": "SA-003-Zigbee",
"name": "eWeLink SA-003-Zigbee",
"sw_version": null,
"entry_type": null,
"id": "262d5b6e6b377055fd1deefbb25b01fe",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": "basement",
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"core_zwave_js"
]
],
"manufacturer": "Official add-ons",
"model": "Home Assistant Add-on",
"name": "Z-Wave JS",
"sw_version": "0.1.45",
"entry_type": "service",
"id": "bc6b9946fb0cfcc330a8296e0e3290ae",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"168e1b19293508dafb169cd8270442a4"
],
"connections": [],
"identifiers": [
[
"zwave_js",
"4189927064-1"
]
],
"manufacturer": "Sigma Designs (Former Zensys)",
"model": "HUSBZB-1",
"name": "QuickStick Combo",
"sw_version": "4.32",
"entry_type": null,
"id": "3017c2b633b22d41be13f03bd56672f0",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"168e1b19293508dafb169cd8270442a4"
],
"connections": [],
"identifiers": [
[
"zwave_js",
"4189927064-2"
]
],
"manufacturer": "Nortek Security & Control LLC",
"model": "PS15Z",
"name": "Plug-In Appliance Module",
"sw_version": "5.41",
"entry_type": null,
"id": "b43320eeef9b58d28234494e0aef12b8",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"00:12:4b:00:23:b7:78:94"
]
],
"identifiers": [
[
"zha",
"00:12:4b:00:23:b7:78:94"
]
],
"manufacturer": "SONOFF",
"model": "S31 Lite zb",
"name": "SONOFF S31 Lite zb",
"sw_version": null,
"entry_type": null,
"id": "9f5c4bbc8fe9161e5f20e44bb9bbacef",
"via_device_id": "f05eae618c3d4f908b9022a05e88ce95",
"area_id": "",
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"45df7312_zigbee2mqtt_edge"
]
],
"manufacturer": "Home Assistant Add-on: Zigbee2mqtt",
"model": "Home Assistant Add-on",
"name": "Zigbee2mqtt Edge",
"sw_version": "edge",
"entry_type": "service",
"id": "dfb728261d65045160bc85ec10bb43bf",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"487ba0d979a943a6b590f834f80258d0"
],
"connections": [],
"identifiers": [
[
"mqtt",
"zigbee2mqtt_0x00158d00070b32d6"
]
],
"manufacturer": "Xiaomi",
"model": "Aqara door & window contact sensor (MCCGQ11LM)",
"name": "Door",
"sw_version": "Zigbee2MQTT 1.21.1-dev",
"entry_type": null,
"id": "14e3db9dff274f58c9065e162ee7b8a6",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"a0d7b954_appdaemon"
]
],
"manufacturer": "Home Assistant Community Add-ons",
"model": "Home Assistant Add-on",
"name": "AppDaemon 4",
"sw_version": "0.7.0",
"entry_type": "service",
"id": "a34971bea044ae5aa10d467f25c77bf9",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
},
{
"config_entries": [
"168e1b19293508dafb169cd8270442a4"
],
"connections": [],
"identifiers": [
[
"zwave_js",
"4189927064-3"
]
],
"manufacturer": "Wink Inc.",
"model": "D/W SENSOR",
"name": "Door/Window Sensor",
"sw_version": "3.80",
"entry_type": null,
"id": "576d2874d1e67620b848c789be7d9e07",
"via_device_id": null,
"area_id": null,
"name_by_user": "Zwave Test Sensor",
"disabled_by": null
},
{
"config_entries": [
"487ba0d979a943a6b590f834f80258d0"
],
"connections": [],
"identifiers": [
[
"mqtt",
"zigbee2mqtt_0x00158d0006f39e88"
]
],
"manufacturer": "Xiaomi",
"model": "Aqara wireless switch (WXKG11LM)",
"name": "Office Switch",
"sw_version": "Zigbee2MQTT 1.21.1-dev",
"entry_type": null,
"id": "c587194eb713c5cc45e0fef31080796b",
"via_device_id": null,
"area_id": null,
"name_by_user": null,
"disabled_by": null
}
],
"deleted_devices": [
{
"config_entries": [
"5b411d516be8bea4ccc31c46e6a36c0c"
],
"connections": [],
"identifiers": [
[
"hassio",
"45df7312_zigbee2mqtt"
]
],
"id": "02f26deab8f5be50ea518504ffab750f",
"orphaned_timestamp": null
},
{
"config_entries": [
"babd9cc14c314f6db8c5ce578dfa9564"
],
"connections": [
[
"zigbee",
"00:15:8d:00:07:0b:32:d6"
]
],
"identifiers": [
[
"zha",
"00:15:8d:00:07:0b:32:d6"
]
],
"id": "d353ec958e7f6bff604a280eb32fe472",
"orphaned_timestamp": null
}
]
}
}

File diff suppressed because it is too large Load Diff

818
.storage/core.restore_state Normal file
View File

@@ -0,0 +1,818 @@
{
"version": 1,
"key": "core.restore_state",
"data": [
{
"state": {
"entity_id": "person.dan",
"state": "home",
"attributes": {
"editable": false,
"id": "dan",
"latitude": 41.399037,
"longitude": -82.2331938,
"gps_accuracy": 11,
"source": "device_tracker.sm_n960u",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"friendly_name": "Dan"
},
"last_changed": "2021-11-04T05:13:46.408785+00:00",
"last_updated": "2021-11-04T05:14:08.377704+00:00",
"context": {
"id": "95eebc813860505bd3e4287ab2eb2565",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "input_boolean.sleeping",
"state": "off",
"attributes": {
"editable": true,
"friendly_name": "sleeping"
},
"last_changed": "2021-11-04T05:13:52.487414+00:00",
"last_updated": "2021-11-04T05:13:52.487414+00:00",
"context": {
"id": "5b0bbb1f93020aac91d380d9a3f4c5b4",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "input_boolean.thermostat_mode",
"state": "on",
"attributes": {
"editable": true,
"friendly_name": "thermostat_mode"
},
"last_changed": "2021-11-04T05:13:52.488063+00:00",
"last_updated": "2021-11-04T05:13:52.488063+00:00",
"context": {
"id": "9953f1b2aaecb34e721e5e456610b84f",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_battery_level",
"state": "40",
"attributes": {
"unit_of_measurement": "%",
"friendly_name": "SM-N960U Battery Level",
"icon": "mdi:battery-40",
"device_class": "battery"
},
"last_changed": "2021-11-04T05:14:05.497508+00:00",
"last_updated": "2021-11-04T05:14:05.497508+00:00",
"context": {
"id": "3346e60960804ef1b9937ada634a62b7",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_battery_state",
"state": "discharging",
"attributes": {
"friendly_name": "SM-N960U Battery State",
"icon": "mdi:battery-minus",
"device_class": "battery"
},
"last_changed": "2021-11-04T05:14:05.498621+00:00",
"last_updated": "2021-11-04T05:14:05.498621+00:00",
"context": {
"id": "475654b0adab3b5eb54f0fb4f77c88e5",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_wifi_connection",
"state": "DD",
"attributes": {
"is_hidden": false,
"friendly_name": "SM-N960U Wifi Connection",
"icon": "mdi:wifi"
},
"last_changed": "2021-11-04T05:14:05.499582+00:00",
"last_updated": "2021-11-04T05:14:05.499582+00:00",
"context": {
"id": "1de69abbf1fc179cc4a79da504ac81be",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_bluetooth_connection",
"state": "0",
"attributes": {
"connected_not_paired_devices": "[]",
"connected_paired_devices": "[]",
"paired_devices": "[88:E0:34:36:91:72, 00:06:66:1E:27:3A, 00:1E:AE:9D:26:78]",
"unit_of_measurement": "connection(s)",
"friendly_name": "SM-N960U Bluetooth Connection",
"icon": "mdi:bluetooth"
},
"last_changed": "2021-11-04T05:14:05.500458+00:00",
"last_updated": "2021-11-04T05:14:05.500458+00:00",
"context": {
"id": "15c94add5b754c5aedc5151c025d20fc",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_geocoded_location",
"state": "353 Seeley St, Amherst, OH 44001, USA",
"attributes": {
"Administrative Area": "Ohio",
"Country": "United States",
"ISO Country Code": "US",
"Latitude": 41.399014,
"Locality": "Amherst",
"Longitude": -82.23327,
"Postal Code": "44001",
"Sub Administrative Area": "Lorain County",
"Sub Locality": "null",
"Sub Thoroughfare": "353",
"Thoroughfare": "Seeley Street",
"friendly_name": "SM-N960U Geocoded Location",
"icon": "mdi:map"
},
"last_changed": "2021-11-04T05:14:05.501326+00:00",
"last_updated": "2021-11-04T05:14:05.501326+00:00",
"context": {
"id": "6d7704b3ee7e56dd2caa255a103fcf39",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_light_sensor",
"state": "9",
"attributes": {
"unit_of_measurement": "lx",
"friendly_name": "SM-N960U Light Sensor",
"icon": "mdi:brightness-5",
"device_class": "illuminance"
},
"last_changed": "2021-11-04T05:14:05.502176+00:00",
"last_updated": "2021-11-04T05:14:05.502176+00:00",
"context": {
"id": "6a0fca08dee08e01bb82cc1b40ca8848",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_next_alarm",
"state": "2021-11-04T13:00:00.000Z",
"attributes": {
"Local Time": "Thu Nov 04 09:00:00 EDT 2021",
"Package": "com.urbandroid.sleep",
"Time in Milliseconds": 1636030800000,
"friendly_name": "SM-N960U Next Alarm",
"icon": "mdi:alarm",
"device_class": "timestamp"
},
"last_changed": "2021-11-04T05:14:05.503030+00:00",
"last_updated": "2021-11-04T05:14:05.503030+00:00",
"context": {
"id": "0b53def8535d77ca1d0d1226caba5e56",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_storage_sensor",
"state": "33",
"attributes": {
"Free internal storage": "37GB",
"Total internal storage": "110GB",
"unit_of_measurement": "%",
"friendly_name": "SM-N960U Storage Sensor",
"icon": "mdi:harddisk"
},
"last_changed": "2021-11-04T05:14:05.503878+00:00",
"last_updated": "2021-11-04T05:14:05.503878+00:00",
"context": {
"id": "4f662d6492e980be5258fcd743e0e065",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_audio_sensor",
"state": "normal",
"attributes": {
"friendly_name": "SM-N960U Audio Sensor",
"icon": "mdi:volume-high"
},
"last_changed": "2021-11-04T05:14:05.504892+00:00",
"last_updated": "2021-11-04T05:14:05.504892+00:00",
"context": {
"id": "ff9aaf5f8dd93951b77c337c845037a3",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_do_not_disturb_sensor",
"state": "off",
"attributes": {
"friendly_name": "SM-N960U Do Not Disturb Sensor",
"icon": "mdi:minus-circle"
},
"last_changed": "2021-11-04T05:14:05.505777+00:00",
"last_updated": "2021-11-04T05:14:05.505777+00:00",
"context": {
"id": "87f2d8a0c1b84b64250481f22158a3aa",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_last_reboot",
"state": "2021-10-18T19:15:27Z",
"attributes": {
"Local Time": "Mon Oct 18 15:15:27 EDT 2021",
"Time in Milliseconds": 1634584527588,
"friendly_name": "SM-N960U Last Reboot",
"icon": "mdi:restart",
"device_class": "timestamp"
},
"last_changed": "2021-11-04T05:14:05.506643+00:00",
"last_updated": "2021-11-04T05:14:05.506643+00:00",
"context": {
"id": "8c5162b3e93e75d68c88bd0c263693e9",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_pressure_sensor",
"state": "1004.3",
"attributes": {
"unit_of_measurement": "hPa",
"friendly_name": "SM-N960U Pressure Sensor",
"icon": "mdi:gauge",
"device_class": "pressure"
},
"last_changed": "2021-11-04T05:14:05.507497+00:00",
"last_updated": "2021-11-04T05:14:05.507497+00:00",
"context": {
"id": "b728aa2ec0e7b0c7d00a5e9a40065fcd",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_proximity_sensor",
"state": "8",
"attributes": {
"friendly_name": "SM-N960U Proximity Sensor",
"icon": "mdi:leak"
},
"last_changed": "2021-11-04T05:14:05.508351+00:00",
"last_updated": "2021-11-04T05:14:05.508351+00:00",
"context": {
"id": "b18f2822f6764866ca724b9a6dc516e5",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_charger_type",
"state": "none",
"attributes": {
"friendly_name": "SM-N960U Charger Type",
"icon": "mdi:battery"
},
"last_changed": "2021-11-04T05:14:05.509209+00:00",
"last_updated": "2021-11-04T05:14:05.509209+00:00",
"context": {
"id": "cab1705f444adf645792386c43c4e339",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_battery_health",
"state": "good",
"attributes": {
"friendly_name": "SM-N960U Battery Health",
"icon": "mdi:battery-heart-variant"
},
"last_changed": "2021-11-04T05:14:05.510046+00:00",
"last_updated": "2021-11-04T05:14:05.510046+00:00",
"context": {
"id": "244be23fdabffb8b4ea0b61b1ed7140a",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sm_n960u_battery_temperature",
"state": "21.1",
"attributes": {
"unit_of_measurement": "\u00b0F",
"friendly_name": "SM-N960U Battery Temperature",
"icon": "mdi:battery",
"device_class": "temperature"
},
"last_changed": "2021-11-04T05:14:05.510912+00:00",
"last_updated": "2021-11-04T05:14:05.510912+00:00",
"context": {
"id": "7bc0575c2888443928183f54e66127dc",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "binary_sensor.sm_n960u_is_charging",
"state": "off",
"attributes": {
"friendly_name": "SM-N960U Is Charging",
"icon": "mdi:power-plug-off",
"device_class": "plug"
},
"last_changed": "2021-11-04T05:14:05.511812+00:00",
"last_updated": "2021-11-04T05:14:05.511812+00:00",
"context": {
"id": "3342a971325ccfe14627403f8d17ec81",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "device_tracker.sm_n960u",
"state": "home",
"attributes": {
"source_type": "gps",
"latitude": 41.399037,
"longitude": -82.2331938,
"gps_accuracy": 11,
"altitude": 172.0,
"course": 0,
"speed": 0,
"vertical_accuracy": 3,
"friendly_name": "Dans Phone"
},
"last_changed": "2021-11-04T05:14:05.617412+00:00",
"last_updated": "2021-11-04T05:14:05.617412+00:00",
"context": {
"id": "0db767e3b3c40817a15c400461939be2",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "device_tracker.dan_dansphone",
"state": "home",
"attributes": {
"source_type": "gps",
"battery_level": 74,
"latitude": 41.3990354,
"longitude": -82.2331867,
"gps_accuracy": 11,
"friendly_name": "Dan"
},
"last_changed": "2021-11-04T05:14:05.805204+00:00",
"last_updated": "2021-11-04T05:14:05.805204+00:00",
"context": {
"id": "c3b74675c117e4e49aa810a1b6143e41",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:14:08.459128+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_1d3e0b03_smartenergy_metering",
"state": "0.0",
"attributes": {
"state_class": "measurement",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "W",
"friendly_name": "sengled E11-G13 1d3e0b03 smartenergy_metering",
"device_class": "power"
},
"last_changed": "2021-10-16T16:24:26.176723+00:00",
"last_updated": "2021-10-16T16:24:26.176723+00:00",
"context": {
"id": "49cfe0ff36514b4af8b072c16314bcac",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_fd3e0b03_smartenergy_metering",
"state": "0.0",
"attributes": {
"state_class": "measurement",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "W",
"friendly_name": "sengled E11-G13 fd3e0b03 smartenergy_metering",
"device_class": "power"
},
"last_changed": "2021-10-16T16:24:26.186477+00:00",
"last_updated": "2021-10-16T16:24:26.186477+00:00",
"context": {
"id": "a22aabe45def5a96e4443fe15353c503",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_ba220b03_smartenergy_metering",
"state": "0.0",
"attributes": {
"state_class": "measurement",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "W",
"friendly_name": "sengled E11-G13 ba220b03 smartenergy_metering",
"device_class": "power"
},
"last_changed": "2021-10-16T16:24:26.195594+00:00",
"last_updated": "2021-10-16T16:24:26.195594+00:00",
"context": {
"id": "9393768168ad98f6a66d28236affbdea",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_d6681403_smartenergy_metering",
"state": "0.0",
"attributes": {
"state_class": "measurement",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "W",
"friendly_name": "sengled E11-G13 d6681403 smartenergy_metering",
"device_class": "power"
},
"last_changed": "2021-10-16T16:24:26.203577+00:00",
"last_updated": "2021-10-16T16:24:26.203577+00:00",
"context": {
"id": "f7c792c8652b800ba4543afbda30e7fc",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "light.sengled_e11_g13_1d3e0b03_level_on_off",
"state": "off",
"attributes": {
"supported_color_modes": [
"brightness"
],
"off_brightness": null,
"friendly_name": "Entry Way",
"supported_features": 41
},
"last_changed": "2021-11-03T19:02:52.766372+00:00",
"last_updated": "2021-11-03T19:02:52.766372+00:00",
"context": {
"id": "a026b1a46e98bb86de7020317d6db862",
"parent_id": null,
"user_id": "e2ac2db8725247b0bb84b03718a01c39"
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "light.sengled_e11_g13_fd3e0b03_level_on_off",
"state": "on",
"attributes": {
"supported_color_modes": [
"brightness"
],
"color_mode": "brightness",
"brightness": 254,
"off_brightness": null,
"friendly_name": "Dinning Rom",
"supported_features": 41
},
"last_changed": "2021-11-04T02:54:47.852214+00:00",
"last_updated": "2021-11-04T02:54:47.852214+00:00",
"context": {
"id": "fed24a2309b0693ebc1a686aa8929fbf",
"parent_id": null,
"user_id": "e2ac2db8725247b0bb84b03718a01c39"
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "light.sengled_e11_g13_ba220b03_level_on_off",
"state": "on",
"attributes": {
"supported_color_modes": [
"brightness"
],
"color_mode": "brightness",
"brightness": 254,
"off_brightness": null,
"friendly_name": "Living Room",
"supported_features": 41
},
"last_changed": "2021-11-04T02:54:49.821120+00:00",
"last_updated": "2021-11-04T02:54:49.821120+00:00",
"context": {
"id": "51feff96d1757df097d5b32378f3234d",
"parent_id": null,
"user_id": "e2ac2db8725247b0bb84b03718a01c39"
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "light.sengled_e11_g13_d6681403_level_on_off",
"state": "off",
"attributes": {
"supported_color_modes": [
"brightness"
],
"off_brightness": null,
"friendly_name": "Office",
"supported_features": 41
},
"last_changed": "2021-11-04T02:54:29.770893+00:00",
"last_updated": "2021-11-04T02:54:29.770893+00:00",
"context": {
"id": "43cbf18fb0b0d9c32fb05c2b3ef84736",
"parent_id": null,
"user_id": "e2ac2db8725247b0bb84b03718a01c39"
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_af400b03_smartenergy_metering",
"state": "unavailable",
"attributes": {
"state_class": "measurement",
"unit_of_measurement": "W",
"friendly_name": "sengled E11-G13 af400b03 smartenergy_metering",
"device_class": "power"
},
"last_changed": "2021-10-18T21:19:45.565788+00:00",
"last_updated": "2021-10-18T21:19:45.565788+00:00",
"context": {
"id": "800ad05f1a8eb77f0956aadd2ea75917",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "light.sengled_e11_g13_af400b03_level_on_off",
"state": "unavailable",
"attributes": {
"supported_color_modes": [
"brightness"
],
"friendly_name": "sengled E11-G13 af400b03 level, on_off",
"supported_features": 41
},
"last_changed": "2021-10-18T21:19:45.567171+00:00",
"last_updated": "2021-10-18T21:19:45.567171+00:00",
"context": {
"id": "09d6691729b885ee199dc24dcec127f7",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "switch.ewelink_sa_003_zigbee_33eeea22_on_off",
"state": "off",
"attributes": {
"friendly_name": "Grow Fan"
},
"last_changed": "2021-11-04T01:30:38.495714+00:00",
"last_updated": "2021-11-04T01:30:38.495714+00:00",
"context": {
"id": "bc5be675ff9831e771a6af652e942cf2",
"parent_id": null,
"user_id": "e2ac2db8725247b0bb84b03718a01c39"
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "switch.sonoff_s31_lite_zb_9478b723_on_off",
"state": "off",
"attributes": {
"friendly_name": "Dinning Room Switch"
},
"last_changed": "2021-11-04T04:00:30.856952+00:00",
"last_updated": "2021-11-04T04:00:30.856952+00:00",
"context": {
"id": "1d4c560ea0ac84de0a13512e5d0308fb",
"parent_id": null,
"user_id": "e2ac2db8725247b0bb84b03718a01c39"
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_1d3e0b03_smartenergy_metering_summation_delivered",
"state": "3.679",
"attributes": {
"state_class": "total_increasing",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "kWh",
"friendly_name": "sengled E11-G13 1d3e0b03 smartenergy_metering summation_delivered",
"device_class": "energy"
},
"last_changed": "2021-10-16T16:24:26.182056+00:00",
"last_updated": "2021-10-16T16:24:26.182056+00:00",
"context": {
"id": "fa83264b523ca69852a66bb380e804f5",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_fd3e0b03_smartenergy_metering_summation_delivered",
"state": "72.683",
"attributes": {
"state_class": "total_increasing",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "kWh",
"friendly_name": "sengled E11-G13 fd3e0b03 smartenergy_metering summation_delivered",
"device_class": "energy"
},
"last_changed": "2021-10-16T16:24:26.190871+00:00",
"last_updated": "2021-10-16T16:24:26.190871+00:00",
"context": {
"id": "913ad5b77c65bf860bd0291db183aa99",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_ba220b03_smartenergy_metering_summation_delivered",
"state": "85.364",
"attributes": {
"state_class": "total_increasing",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "kWh",
"friendly_name": "sengled E11-G13 ba220b03 smartenergy_metering summation_delivered",
"device_class": "energy"
},
"last_changed": "2021-10-16T16:24:26.199573+00:00",
"last_updated": "2021-10-16T16:24:26.199573+00:00",
"context": {
"id": "3b95d4f04e71ea6aeefa7b6467796c2d",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_d6681403_smartenergy_metering_summation_delivered",
"state": "74.519",
"attributes": {
"state_class": "total_increasing",
"device_type": "Electric Metering",
"status": "NO_ALARMS",
"unit_of_measurement": "kWh",
"friendly_name": "sengled E11-G13 d6681403 smartenergy_metering summation_delivered",
"device_class": "energy"
},
"last_changed": "2021-10-16T16:24:26.207400+00:00",
"last_updated": "2021-10-16T16:24:26.207400+00:00",
"context": {
"id": "4d2cfd95c15c4131eca477133f44fafc",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
},
{
"state": {
"entity_id": "sensor.sengled_e11_g13_af400b03_smartenergy_metering_summation_delivered",
"state": "unavailable",
"attributes": {
"state_class": "total_increasing",
"unit_of_measurement": "kWh",
"friendly_name": "sengled E11-G13 af400b03 smartenergy_metering summation_delivered",
"device_class": "energy"
},
"last_changed": "2021-10-18T21:19:45.566571+00:00",
"last_updated": "2021-10-18T21:19:45.566571+00:00",
"context": {
"id": "17d55716b8c3b76afcedb3c0c1723bf9",
"parent_id": null,
"user_id": null
}
},
"last_seen": "2021-11-04T05:08:04.659722+00:00"
}
]
}

7
.storage/core.uuid Normal file
View File

@@ -0,0 +1,7 @@
{
"data": {
"uuid": "4c0daec8e41144b3b5db3b54c0c0fbb5"
},
"key": "core.uuid",
"version": 1
}

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"key": "frontend.user_data_e2ac2db8725247b0bb84b03718a01c39",
"data": {
"core": {
"showAdvanced": true
},
"language": {
"language": "en",
"number_format": "language",
"time_format": "language"
}
}
}

7
.storage/hassio Normal file
View File

@@ -0,0 +1,7 @@
{
"data": {
"hassio_user": "7e128bb4681b4f9680cff80d395082c8"
},
"key": "hassio",
"version": 1
}

17
.storage/http Normal file
View File

@@ -0,0 +1,17 @@
{
"version": 1,
"key": "http",
"data": {
"use_x_forwarded_for": true,
"trusted_proxies": [
"192.168.86.198"
],
"login_attempts_threshold": -1,
"ip_ban_enabled": true,
"ssl_profile": "modern",
"server_port": 8123,
"cors_allowed_origins": [
"https://cast.home-assistant.io"
]
}
}

16
.storage/input_boolean Normal file
View File

@@ -0,0 +1,16 @@
{
"data": {
"items": [
{
"id": "sleeping",
"name": "sleeping"
},
{
"id": "thermostat_mode",
"name": "thermostat_mode"
}
]
},
"key": "input_boolean",
"version": 1
}

32
.storage/lovelace Normal file
View File

@@ -0,0 +1,32 @@
{
"data": {
"config": {
"title": "Home",
"views": [
{
"badges": [
{
"entity": "binary_sensor.updater"
},
{
"entity": "person.dan"
},
{
"entity": "sun.sun"
}
],
"cards": [
{
"entity": "weather.home",
"type": "weather-forecast"
}
],
"path": "default_view",
"title": "Home"
}
]
}
},
"key": "lovelace",
"version": 1
}

120
.storage/lovelace.20_21 Normal file
View File

@@ -0,0 +1,120 @@
{
"version": 1,
"key": "lovelace.20_21",
"data": {
"config": {
"title": "Home",
"views": [
{
"path": "default_view",
"title": "Home",
"cards": [
{
"type": "weather-forecast",
"entity": "weather.home_2",
"show_forecast": true
},
{
"type": "entities",
"entities": [
{
"entity": "light.table"
},
{
"entity": "light.lamp_left"
},
{
"entity": "light.lamp_right"
},
{
"entity": "light.sengled_e11_g13_d6681403_level_on_off"
},
{
"entity": "light.sengled_e11_g13_1d3e0b03_level_on_off"
},
{
"entity": "light.sengled_e11_g13_ba220b03_level_on_off"
},
{
"entity": "light.sengled_e11_g13_fd3e0b03_level_on_off"
}
],
"title": "Lights",
"show_header_toggle": true,
"state_color": false
},
{
"type": "entities",
"entities": [
{
"entity": "sensor.home_humidity"
},
{
"entity": "sensor.home_temperature"
},
{
"entity": "sensor.thermostat_away_mode"
},
{
"entity": "sensor.thermostat_climate_mode"
},
{
"entity": "sensor.thermostat_current_status"
},
{
"entity": "sensor.thermostat_outside_temperature"
}
],
"title": "Sensor",
"state_color": false
},
{
"type": "glance",
"entities": [
{
"entity": "person.dan"
},
{
"entity": "binary_sensor.door_contact"
}
]
},
{
"type": "entities",
"entities": [
"switch.sonoff_s31_lite_zb_9478b723_on_off",
"switch.plug_in_appliance_module"
],
"title": "Switch"
}
]
},
{
"title": "HVAC",
"path": "hvac",
"badges": [],
"cards": [
{
"type": "thermostat",
"entity": "climate.home"
}
]
},
{
"title": "Grow",
"path": "grow",
"badges": [],
"cards": [
{
"type": "entities",
"entities": [
"switch.ewelink_sa_003_zigbee_33eeea22_on_off"
],
"title": "Basement"
}
]
}
]
}
}
}

View File

@@ -0,0 +1,16 @@
{
"version": 1,
"key": "lovelace_dashboards",
"data": {
"items": [
{
"require_admin": false,
"show_in_sidebar": true,
"title": "21",
"url_path": "20-21",
"mode": "storage",
"id": "20_21"
}
]
}
}

223
.storage/mobile_app Normal file
View File

@@ -0,0 +1,223 @@
{
"version": 1,
"key": "mobile_app",
"data": {
"binary_sensor": {
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_is_charging": {
"added": true,
"attributes": {},
"device_class": "plug",
"icon": "mdi:power-plug-off",
"name": "Is Charging",
"state": false,
"type": "binary_sensor",
"unique_id": "is_charging",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
}
},
"deleted_ids": [],
"sensor": {
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_audio_sensor": {
"added": true,
"attributes": {},
"icon": "mdi:volume-high",
"name": "Audio Sensor",
"state": "normal",
"type": "sensor",
"unique_id": "audio_sensor",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_battery_health": {
"added": true,
"attributes": {},
"icon": "mdi:battery-heart-variant",
"name": "Battery Health",
"state": "good",
"type": "sensor",
"unique_id": "battery_health",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_battery_level": {
"added": true,
"attributes": {},
"device_class": "battery",
"icon": "mdi:battery-80",
"name": "Battery Level",
"state": 81,
"type": "sensor",
"unique_id": "battery_level",
"unit_of_measurement": "%",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_battery_state": {
"added": true,
"attributes": {},
"device_class": "battery",
"icon": "mdi:battery-minus",
"name": "Battery State",
"state": "discharging",
"type": "sensor",
"unique_id": "battery_state",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_bluetooth_connection": {
"added": true,
"attributes": {
"connected_not_paired_devices": "[]",
"connected_paired_devices": "[]",
"paired_devices": "[B0:B4:48:83:AA:66, 00:06:66:1E:27:3A, 00:00:AB:CD:21:05, 08:EB:ED:D5:AB:96, 00:1E:AE:9D:26:78, 91:60:86:65:77:53]"
},
"icon": "mdi:bluetooth",
"name": "Bluetooth Connection",
"state": 0,
"type": "sensor",
"unique_id": "bluetooth_connection",
"unit_of_measurement": "connection(s)",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_charger_type": {
"added": true,
"attributes": {},
"icon": "mdi:battery",
"name": "Charger Type",
"state": "none",
"type": "sensor",
"unique_id": "charger_type",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_dnd_sensor": {
"added": true,
"attributes": {},
"icon": "mdi:do-not-disturb",
"name": "Do Not Disturb Sensor",
"state": "off",
"type": "sensor",
"unique_id": "dnd_sensor",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_geocoded_location": {
"added": true,
"attributes": {
"Administrative Area": "Ohio",
"Country": "United States",
"ISO Country Code": "US",
"Latitude": 41.399014,
"Locality": "Amherst",
"Longitude": -82.23327,
"Postal Code": "44001",
"Sub Administrative Area": "Lorain County",
"Sub Locality": "null",
"Sub Thoroughfare": "353",
"Thoroughfare": "Seeley Street"
},
"icon": "mdi:map",
"name": "Geocoded Location",
"state": "353 Seeley St, Amherst, OH 44001, USA",
"type": "sensor",
"unique_id": "geocoded_location",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_last_reboot": {
"added": true,
"attributes": {
"Local Time": "Wed May 05 04:39:19 EDT 2021",
"Time in Milliseconds": 1620203959196
},
"device_class": "timestamp",
"icon": "mdi:restart",
"name": "Last Reboot",
"state": "2021-05-05T08:39:19Z",
"type": "sensor",
"unique_id": "last_reboot",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_light_sensor": {
"added": true,
"attributes": {},
"device_class": "illuminance",
"icon": "mdi:brightness-5",
"name": "Light Sensor",
"state": "19",
"type": "sensor",
"unique_id": "light_sensor",
"unit_of_measurement": "lx",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_next_alarm": {
"added": true,
"attributes": {
"Local Time": "Thu May 20 08:00:00 EDT 2021",
"Package": "com.urbandroid.sleep",
"Time in Milliseconds": 1621512000000
},
"device_class": "timestamp",
"icon": "mdi:alarm",
"name": "Next Alarm",
"state": "2021-05-20T12:00:00.000Z",
"type": "sensor",
"unique_id": "next_alarm",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_pressure_sensor": {
"added": true,
"attributes": {},
"device_class": "pressure",
"icon": "mdi:gauge",
"name": "Pressure Sensor",
"state": "1002.5",
"type": "sensor",
"unique_id": "pressure_sensor",
"unit_of_measurement": "hPa",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_proximity_sensor": {
"added": true,
"attributes": {},
"icon": "mdi:leak",
"name": "Proximity Sensor",
"state": 8,
"type": "sensor",
"unique_id": "proximity_sensor",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_storage_sensor": {
"added": true,
"attributes": {
"Free internal storage": "50GB",
"Total internal storage": "110GB"
},
"icon": "mdi:harddisk",
"name": "Storage Sensor",
"state": 45,
"type": "sensor",
"unique_id": "storage_sensor",
"unit_of_measurement": "%",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_wifi_connection": {
"added": true,
"attributes": {
"is_hidden": false
},
"icon": "mdi:wifi",
"name": "Wifi Connection",
"state": "DD",
"type": "sensor",
"unique_id": "wifi_connection",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797"
},
"eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797_battery_temperature": {
"unique_id": "battery_temperature",
"state": 28.8,
"type": "sensor",
"icon": "mdi:battery",
"attributes": {},
"name": "Battery Temperature",
"device_class": "temperature",
"unit_of_measurement": "\u00b0C",
"webhook_id": "eb9d2fbd9f8fea4200d7752b689aecfe6a2ca2215d2cbe0600b75a4fd20a6797",
"added": true
}
}
}
}

11
.storage/onboarding Normal file
View File

@@ -0,0 +1,11 @@
{
"data": {
"done": [
"user",
"core_config",
"integration"
]
},
"key": "onboarding",
"version": 3
}

17
.storage/person Normal file
View File

@@ -0,0 +1,17 @@
{
"version": 2,
"key": "person",
"data": {
"items": [
{
"device_trackers": [
"device_tracker.sm_n960u"
],
"id": "dan",
"name": "Dan",
"user_id": "e2ac2db8725247b0bb84b03718a01c39",
"picture": null
}
]
}
}

53
.storage/zha.storage Normal file
View File

@@ -0,0 +1,53 @@
{
"version": 1,
"key": "zha.storage",
"data": {
"devices": [
{
"name": "Silicon Labs EZSP",
"ieee": "00:0d:6f:00:0a:ff:73:f8",
"last_seen": 1636001002.7798755
},
{
"name": "sengled E11-G13",
"ieee": "b0:ce:18:14:03:0b:3e:1d",
"last_seen": 1636002128.3008697
},
{
"name": "sengled E11-G13",
"ieee": "b0:ce:18:14:03:0b:3e:fd",
"last_seen": 1636002335.1381133
},
{
"name": "sengled E11-G13",
"ieee": "b0:ce:18:14:03:0b:22:ba",
"last_seen": 1636002187.7172956
},
{
"name": "sengled E11-G13",
"ieee": "b0:ce:18:14:03:14:68:d6",
"last_seen": 1636002481.4106457
},
{
"name": "sengled E11-G13",
"ieee": "b0:ce:18:14:03:0b:40:af",
"last_seen": 1634584548.0082166
},
{
"name": "LUMI lumi.sensor_magnet.aq2",
"ieee": "00:15:8d:00:07:0b:32:d6",
"last_seen": 1631821141.6237457
},
{
"name": "unk_manufacturer unk_model",
"ieee": "00:12:4b:00:22:ea:ee:33",
"last_seen": 1636002199.043916
},
{
"name": "SONOFF S31 Lite zb",
"ieee": "00:12:4b:00:23:b7:78:94",
"last_seen": 1636002438.6431177
}
]
}
}

View File

@@ -0,0 +1,73 @@
{
"version": 1,
"key": "zwave_js.legacy_zwave_migration",
"data": {
"168e1b19293508dafb169cd8270442a4": {
"sensor.zwave_test_sensor_battery_level": {
"node_id": 3,
"endpoint_index": 0,
"command_class": 128,
"value_property_name": "level",
"value_property_key_name": null,
"value_id": "3-128-0-level",
"device_id": "576d2874d1e67620b848c789be7d9e07",
"domain": "sensor",
"entity_id": "sensor.zwave_test_sensor_battery_level",
"unique_id": "4189927064.3-128-0-level",
"unit_of_measurement": "%"
},
"binary_sensor.door_window_sensor_access_control_window_door_is_open": {
"node_id": 3,
"endpoint_index": 0,
"command_class": 113,
"value_property_name": "Access Control",
"value_property_key_name": "Door state",
"value_id": "3-113-0-Access Control-Door state",
"device_id": "576d2874d1e67620b848c789be7d9e07",
"domain": "binary_sensor",
"entity_id": "binary_sensor.door_window_sensor_access_control_window_door_is_open",
"unique_id": "4189927064.3-113-0-Access Control-Door state.22",
"unit_of_measurement": null
},
"binary_sensor.zwave_test_sensor_low_battery_level": {
"node_id": 3,
"endpoint_index": 0,
"command_class": 128,
"value_property_name": "isLow",
"value_property_key_name": null,
"value_id": "3-128-0-isLow",
"device_id": "576d2874d1e67620b848c789be7d9e07",
"domain": "binary_sensor",
"entity_id": "binary_sensor.zwave_test_sensor_low_battery_level",
"unique_id": "4189927064.3-128-0-isLow",
"unit_of_measurement": null
},
"switch.plug_in_appliance_module": {
"node_id": 2,
"endpoint_index": 0,
"command_class": 37,
"value_property_name": "currentValue",
"value_property_key_name": null,
"value_id": "2-37-0-currentValue",
"device_id": "b43320eeef9b58d28234494e0aef12b8",
"domain": "switch",
"entity_id": "switch.plug_in_appliance_module",
"unique_id": "4189927064.2-37-0-currentValue",
"unit_of_measurement": null
},
"select.plug_in_appliance_module_local_protection_state": {
"node_id": 2,
"endpoint_index": 0,
"command_class": 117,
"value_property_name": "local",
"value_property_key_name": null,
"value_id": "2-117-0-local",
"device_id": "b43320eeef9b58d28234494e0aef12b8",
"domain": "select",
"entity_id": "select.plug_in_appliance_module_local_protection_state",
"unique_id": "4189927064.2-117-0-local",
"unit_of_measurement": null
}
}
}
}

311
OZW_Log.txt Normal file
View File

@@ -0,0 +1,311 @@
2021-09-16 15:12:31.001 Always, OpenZwave Version 1.4.3469 Starting Up
2021-09-16 15:12:43.568 Info, Setting Up Provided Network Key for Secure Communications
2021-09-16 15:12:43.569 Info, mgr, Added driver for controller /dev/ttyUSB0
2021-09-16 15:12:43.569 Info, Opening controller /dev/ttyUSB0
2021-09-16 15:12:43.570 Info, Trying to open serial port /dev/ttyUSB0 (attempt 1)
2021-09-16 15:12:43.574 Info, Serial port /dev/ttyUSB0 opened (attempt 1)
2021-09-16 15:12:43.575 Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_VERSION: 0x01, 0x03, 0x00, 0x15, 0xe9
2021-09-16 15:12:43.575 Detail, contrlr, Queuing (Command) FUNC_ID_ZW_MEMORY_GET_ID: 0x01, 0x03, 0x00, 0x20, 0xdc
2021-09-16 15:12:43.575 Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_CONTROLLER_CAPABILITIES: 0x01, 0x03, 0x00, 0x05, 0xf9
2021-09-16 15:12:43.575 Detail, contrlr, Queuing (Command) FUNC_ID_SERIAL_API_GET_CAPABILITIES: 0x01, 0x03, 0x00, 0x07, 0xfb
2021-09-16 15:12:43.575 Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_SUC_NODE_ID: 0x01, 0x03, 0x00, 0x56, 0xaa
2021-09-16 15:12:43.576 Detail,
2021-09-16 15:12:43.576 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x15) - FUNC_ID_ZW_GET_VERSION: 0x01, 0x03, 0x00, 0x15, 0xe9
2021-09-16 15:12:43.580 Detail, contrlr, Received: 0x01, 0x10, 0x01, 0x15, 0x5a, 0x2d, 0x57, 0x61, 0x76, 0x65, 0x20, 0x34, 0x2e, 0x30, 0x35, 0x00, 0x01, 0x97
2021-09-16 15:12:43.580 Detail,
2021-09-16 15:12:43.580 Info, contrlr, Received reply to FUNC_ID_ZW_GET_VERSION:
2021-09-16 15:12:43.580 Info, contrlr, Static Controller library, version Z-Wave 4.05
2021-09-16 15:12:43.580 Detail, Node045, Expected reply was received
2021-09-16 15:12:43.580 Detail, Node045, Message transaction complete
2021-09-16 15:12:43.580 Detail,
2021-09-16 15:12:43.580 Detail, contrlr, Removing current message
2021-09-16 15:12:43.581 Detail,
2021-09-16 15:12:43.581 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x20) - FUNC_ID_ZW_MEMORY_GET_ID: 0x01, 0x03, 0x00, 0x20, 0xdc
2021-09-16 15:12:43.590 Detail, contrlr, Received: 0x01, 0x08, 0x01, 0x20, 0xf9, 0xbd, 0x36, 0x98, 0x01, 0x3d
2021-09-16 15:12:43.590 Detail,
2021-09-16 15:12:43.590 Info, contrlr, Received reply to FUNC_ID_ZW_MEMORY_GET_ID. Home ID = 0xf9bd3698. Our node ID = 1
2021-09-16 15:12:43.590 Detail, Node189, Expected reply was received
2021-09-16 15:12:43.590 Detail, Node189, Message transaction complete
2021-09-16 15:12:43.590 Detail,
2021-09-16 15:12:43.590 Detail, contrlr, Removing current message
2021-09-16 15:12:43.590 Detail,
2021-09-16 15:12:43.590 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x05) - FUNC_ID_ZW_GET_CONTROLLER_CAPABILITIES: 0x01, 0x03, 0x00, 0x05, 0xf9
2021-09-16 15:12:43.593 Detail, contrlr, Received: 0x01, 0x04, 0x01, 0x05, 0x1c, 0xe3
2021-09-16 15:12:43.594 Detail,
2021-09-16 15:12:43.594 Info, contrlr, Received reply to FUNC_ID_ZW_GET_CONTROLLER_CAPABILITIES:
2021-09-16 15:12:43.594 Info, contrlr, There is a SUC ID Server (SIS) in this network.
2021-09-16 15:12:43.594 Info, contrlr, The PC controller is an inclusion static update controller (SUC) and was the original primary before the SIS was added.
2021-09-16 15:12:43.594 Detail, Node227, Expected reply was received
2021-09-16 15:12:43.594 Detail, Node227, Message transaction complete
2021-09-16 15:12:43.594 Detail,
2021-09-16 15:12:43.594 Detail, contrlr, Removing current message
2021-09-16 15:12:43.594 Detail,
2021-09-16 15:12:43.594 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x07) - FUNC_ID_SERIAL_API_GET_CAPABILITIES: 0x01, 0x03, 0x00, 0x07, 0xfb
2021-09-16 15:12:43.602 Detail, contrlr, Received: 0x01, 0x2b, 0x01, 0x07, 0x04, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0xfe, 0x81, 0xff, 0x88, 0xcf, 0x1f, 0x00, 0x00, 0xfb, 0x9f, 0x7d, 0xa0, 0x67, 0x00, 0x80, 0x80, 0x00, 0x80, 0x86, 0x00, 0x00, 0x00, 0xe8, 0x73, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x60, 0x00, 0x00, 0x03
2021-09-16 15:12:43.602 Detail,
2021-09-16 15:12:43.602 Info, contrlr, Received reply to FUNC_ID_SERIAL_API_GET_CAPABILITIES
2021-09-16 15:12:43.602 Info, contrlr, Serial API Version: 4.32
2021-09-16 15:12:43.603 Info, contrlr, Manufacturer ID: 0x0000
2021-09-16 15:12:43.603 Info, contrlr, Product Type: 0x0001
2021-09-16 15:12:43.603 Info, contrlr, Product ID: 0x0001
2021-09-16 15:12:43.603 Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_RANDOM: 0x01, 0x04, 0x00, 0x1c, 0x20, 0xc7
2021-09-16 15:12:43.603 Detail, contrlr, Queuing (Command) FUNC_ID_SERIAL_API_GET_INIT_DATA: 0x01, 0x03, 0x00, 0x02, 0xfe
2021-09-16 15:12:43.603 Detail, contrlr, Queuing (Command) FUNC_ID_SERIAL_API_SET_TIMEOUTS: 0x01, 0x05, 0x00, 0x06, 0x64, 0x0f, 0x97
2021-09-16 15:12:43.604 Detail, contrlr, Queuing (Command) FUNC_ID_SERIAL_API_APPL_NODE_INFORMATION: 0x01, 0x07, 0x00, 0x03, 0x01, 0x02, 0x01, 0x00, 0xf9
2021-09-16 15:12:43.604 Detail, Node032, Expected reply was received
2021-09-16 15:12:43.604 Detail, Node032, Message transaction complete
2021-09-16 15:12:43.604 Detail,
2021-09-16 15:12:43.604 Detail, contrlr, Removing current message
2021-09-16 15:12:43.604 Detail,
2021-09-16 15:12:43.604 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x56) - FUNC_ID_ZW_GET_SUC_NODE_ID: 0x01, 0x03, 0x00, 0x56, 0xaa
2021-09-16 15:12:43.607 Detail, contrlr, Received: 0x01, 0x04, 0x01, 0x56, 0x01, 0xad
2021-09-16 15:12:43.608 Detail,
2021-09-16 15:12:43.608 Info, contrlr, Received reply to GET_SUC_NODE_ID. Node ID = 1
2021-09-16 15:12:43.608 Detail, Node173, Expected reply was received
2021-09-16 15:12:43.608 Detail, Node173, Message transaction complete
2021-09-16 15:12:43.608 Detail,
2021-09-16 15:12:43.608 Detail, contrlr, Removing current message
2021-09-16 15:12:43.608 Detail,
2021-09-16 15:12:43.608 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x1c) - FUNC_ID_ZW_GET_RANDOM: 0x01, 0x04, 0x00, 0x1c, 0x20, 0xc7
2021-09-16 15:12:43.663 Detail, contrlr, Received: 0x01, 0x25, 0x01, 0x1c, 0x01, 0x20, 0xdb, 0xa7, 0xa9, 0xfb, 0xc6, 0xb5, 0x6c, 0xe2, 0x53, 0x4d, 0xf7, 0x0e, 0x4d, 0x33, 0xb2, 0x03, 0xa8, 0x69, 0x03, 0x4b, 0x87, 0x9b, 0x2d, 0xff, 0xe1, 0x08, 0x03, 0x3d, 0x44, 0xb4, 0xa4, 0xb0, 0x69
2021-09-16 15:12:43.663 Detail,
2021-09-16 15:12:43.663 Info, contrlr, Received reply to FUNC_ID_ZW_GET_RANDOM: true
2021-09-16 15:12:43.663 Detail, Node032, Expected reply was received
2021-09-16 15:12:43.664 Detail, Node032, Message transaction complete
2021-09-16 15:12:43.664 Detail,
2021-09-16 15:12:43.664 Detail, contrlr, Removing current message
2021-09-16 15:12:43.664 Detail,
2021-09-16 15:12:43.664 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x02) - FUNC_ID_SERIAL_API_GET_INIT_DATA: 0x01, 0x03, 0x00, 0x02, 0xfe
2021-09-16 15:12:43.721 Detail, contrlr, Received: 0x01, 0x25, 0x01, 0x02, 0x05, 0x08, 0x1d, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0xcf
2021-09-16 15:12:43.721 Detail,
2021-09-16 15:12:43.721 Info, mgr, Driver with Home ID of 0xf9bd3698 is now ready.
2021-09-16 15:12:43.721 Info,
2021-09-16 15:12:43.725 Info, contrlr, Received reply to FUNC_ID_SERIAL_API_GET_INIT_DATA:
2021-09-16 15:12:43.726 Info, contrlr, Node 001 - Known
2021-09-16 15:12:43.726 Detail, Node001, AdvanceQueries queryPending=0 queryRetries=0 queryStage=CacheLoad live=1
2021-09-16 15:12:43.726 Detail, Node001, QueryStage_CacheLoad
2021-09-16 15:12:43.726 Info, Node001, Node Identity Codes: 0000:0001:0001
2021-09-16 15:12:43.726 Detail, Node001, QueryStage_Associations
2021-09-16 15:12:43.726 Detail, Node001, QueryStage_Neighbors
2021-09-16 15:12:43.726 Detail, contrlr, Requesting routing info (neighbor list) for Node 1
2021-09-16 15:12:43.726 Detail, Node001, Queuing (Command) Get Routing Info (Node=1): 0x01, 0x07, 0x00, 0x80, 0x01, 0x00, 0x00, 0x03, 0x7a
2021-09-16 15:12:43.726 Detail, Node001, Queuing (Query) Query Stage Complete (Neighbors)
2021-09-16 15:12:43.726 Info, contrlr, Node 002 - Known
2021-09-16 15:12:43.726 Detail, Node002, AdvanceQueries queryPending=0 queryRetries=0 queryStage=CacheLoad live=1
2021-09-16 15:12:43.726 Detail, Node002, QueryStage_CacheLoad
2021-09-16 15:12:43.726 Info, Node002, Node Identity Codes: 014f:5250:3030
2021-09-16 15:12:43.726 Info, Node002, NoOperation::Set - Routing=true
2021-09-16 15:12:43.727 Detail, Node002, Queuing (NoOp) NoOperation_Set (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x00, 0x00, 0x25, 0x0a, 0xca
2021-09-16 15:12:43.727 Detail, Node002, Queuing (Query) Query Stage Complete (CacheLoad)
2021-09-16 15:12:43.727 Detail, Node008, Expected reply was received
2021-09-16 15:12:43.727 Detail, Node008, Message transaction complete
2021-09-16 15:12:43.727 Detail,
2021-09-16 15:12:43.727 Detail, contrlr, Removing current message
2021-09-16 15:12:43.727 Detail, Node001, Notification: DriverReady
2021-09-16 15:12:43.730 Detail, Node001, Notification: NodeAdded
2021-09-16 15:12:43.735 Detail, Node001, Notification: NodeProtocolInfo
2021-09-16 15:12:43.753 Detail, Node001, Notification: EssentialNodeQueriesComplete
2021-09-16 15:12:43.757 Detail, Node001, Notification: ValueAdded
2021-09-16 15:12:43.765 Detail, Node002, Notification: NodeAdded
2021-09-16 15:12:43.768 Detail, Node002, Notification: NodeProtocolInfo
2021-09-16 15:12:43.771 Detail, Node002, Notification: EssentialNodeQueriesComplete
2021-09-16 15:12:43.788 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.832 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.834 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.836 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.849 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.851 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.854 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.862 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.864 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.869 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.872 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.893 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.901 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.903 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.905 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.913 Detail, Node002, Notification: ValueAdded
2021-09-16 15:12:43.915 Detail, Node002, Notification: NodeNaming
2021-09-16 15:12:43.917 Detail,
2021-09-16 15:12:43.917 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x06) - FUNC_ID_SERIAL_API_SET_TIMEOUTS: 0x01, 0x05, 0x00, 0x06, 0x64, 0x0f, 0x97
2021-09-16 15:12:43.921 Detail, contrlr, Received: 0x01, 0x05, 0x01, 0x06, 0x64, 0x0f, 0x96
2021-09-16 15:12:43.921 Detail,
2021-09-16 15:12:43.921 Info, contrlr, Received reply to FUNC_ID_SERIAL_API_SET_TIMEOUTS
2021-09-16 15:12:43.921 Detail, Node015, Expected reply was received
2021-09-16 15:12:43.921 Detail, Node015, Message transaction complete
2021-09-16 15:12:43.921 Detail,
2021-09-16 15:12:43.921 Detail, contrlr, Removing current message
2021-09-16 15:12:43.921 Detail,
2021-09-16 15:12:43.922 Info, contrlr, Sending (Command) message (Callback ID=0x00, Expected Reply=0x00) - FUNC_ID_SERIAL_API_APPL_NODE_INFORMATION: 0x01, 0x07, 0x00, 0x03, 0x01, 0x02, 0x01, 0x00, 0xf9
2021-09-16 15:12:43.924 Detail, contrlr, Removing current message
2021-09-16 15:12:43.925 Detail,
2021-09-16 15:12:43.925 Info, Node001, Sending (Command) message (Callback ID=0x00, Expected Reply=0x80) - Get Routing Info (Node=1): 0x01, 0x07, 0x00, 0x80, 0x01, 0x00, 0x00, 0x03, 0x7a
2021-09-16 15:12:43.931 Detail, Node001, Received: 0x01, 0x20, 0x01, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c
2021-09-16 15:12:43.931 Detail,
2021-09-16 15:12:43.931 Info, Node001, Received reply to FUNC_ID_ZW_GET_ROUTING_INFO
2021-09-16 15:12:43.932 Info, Node001, Neighbors of this node are:
2021-09-16 15:12:43.932 Info, Node001, Node 2
2021-09-16 15:12:43.932 Detail, Expected reply was received
2021-09-16 15:12:43.932 Detail, Message transaction complete
2021-09-16 15:12:43.932 Detail,
2021-09-16 15:12:43.932 Detail, Node001, Removing current message
2021-09-16 15:12:43.932 Detail,
2021-09-16 15:12:43.932 Info, Node002, Sending (NoOp) message (Callback ID=0x0a, Expected Reply=0x13) - NoOperation_Set (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x00, 0x00, 0x25, 0x0a, 0xca
2021-09-16 15:12:43.939 Detail, Node002, Received: 0x01, 0x04, 0x01, 0x13, 0x01, 0xe8
2021-09-16 15:12:43.939 Detail, Node002, ZW_SEND_DATA delivered to Z-Wave stack
2021-09-16 15:12:43.955 Detail, Node002, Received: 0x01, 0x05, 0x00, 0x13, 0x0a, 0x00, 0xe3
2021-09-16 15:12:43.955 Detail, Node002, ZW_SEND_DATA Request with callback ID 0x0a received (expected 0x0a)
2021-09-16 15:12:43.955 Info, Node002, Request RTT 23 Average Request RTT 23
2021-09-16 15:12:43.956 Detail, Expected callbackId was received
2021-09-16 15:12:43.956 Detail, Expected reply was received
2021-09-16 15:12:43.956 Detail, Message transaction complete
2021-09-16 15:12:43.956 Detail,
2021-09-16 15:12:43.956 Detail, Node002, Removing current message
2021-09-16 15:12:43.956 Detail, Node002, Notification: Notification - NoOperation
2021-09-16 15:12:43.994 Detail, Node001, Query Stage Complete (Neighbors)
2021-09-16 15:12:43.994 Detail, Node001, AdvanceQueries queryPending=0 queryRetries=0 queryStage=Session live=1
2021-09-16 15:12:43.994 Detail, Node001, QueryStage_Session
2021-09-16 15:12:43.994 Detail, Node001, QueryStage_Dynamic
2021-09-16 15:12:43.994 Detail, Node001, QueryStage_Configuration
2021-09-16 15:12:43.994 Detail, Node001, QueryStage_Complete
2021-09-16 15:12:43.994 Warning, CheckCompletedNodeQueries m_allNodesQueried=0 m_awakeNodesQueried=0
2021-09-16 15:12:43.994 Warning, CheckCompletedNodeQueries all=0, deadFound=0 sleepingOnly=0
2021-09-16 15:12:43.994 Detail, Node001, Notification: NodeQueriesComplete
2021-09-16 15:12:44.008 Detail, Node002, Query Stage Complete (CacheLoad)
2021-09-16 15:12:44.008 Detail, Node002, AdvanceQueries queryPending=0 queryRetries=0 queryStage=Associations live=1
2021-09-16 15:12:44.008 Detail, Node002, QueryStage_Associations
2021-09-16 15:12:44.008 Detail, Node002, QueryStage_Neighbors
2021-09-16 15:12:44.008 Detail, Requesting routing info (neighbor list) for Node 2
2021-09-16 15:12:44.009 Detail, Node002, Queuing (Command) Get Routing Info (Node=2): 0x01, 0x07, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, 0x79
2021-09-16 15:12:44.009 Detail, Node002, Queuing (Query) Query Stage Complete (Neighbors)
2021-09-16 15:12:44.009 Detail,
2021-09-16 15:12:44.010 Info, Node002, Sending (Command) message (Callback ID=0x00, Expected Reply=0x80) - Get Routing Info (Node=2): 0x01, 0x07, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, 0x79
2021-09-16 15:12:44.018 Detail, Node002, Received: 0x01, 0x20, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f
2021-09-16 15:12:44.018 Detail,
2021-09-16 15:12:44.018 Info, Node002, Received reply to FUNC_ID_ZW_GET_ROUTING_INFO
2021-09-16 15:12:44.018 Info, Node002, Neighbors of this node are:
2021-09-16 15:12:44.018 Info, Node002, Node 1
2021-09-16 15:12:44.018 Detail, Expected reply was received
2021-09-16 15:12:44.018 Detail, Message transaction complete
2021-09-16 15:12:44.018 Detail,
2021-09-16 15:12:44.018 Detail, Node002, Removing current message
2021-09-16 15:12:44.019 Detail, Node002, Query Stage Complete (Neighbors)
2021-09-16 15:12:44.019 Detail, Node002, AdvanceQueries queryPending=0 queryRetries=0 queryStage=Session live=1
2021-09-16 15:12:44.019 Detail, Node002, QueryStage_Session
2021-09-16 15:12:44.019 Detail, Node002, Queuing (Query) SwitchAllCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x27, 0x02, 0x25, 0x0b, 0xee
2021-09-16 15:12:44.019 Detail, Node002, Queuing (Query) PowerlevelCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x73, 0x02, 0x25, 0x0c, 0xbd
2021-09-16 15:12:44.019 Detail, Node002, Queuing (Query) ProtectionCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x75, 0x02, 0x25, 0x0d, 0xba
2021-09-16 15:12:44.019 Detail, Node002, Queuing (Query) Query Stage Complete (Session)
2021-09-16 15:12:44.019 Detail,
2021-09-16 15:12:44.019 Info, Node002, Sending (Query) message (Callback ID=0x0b, Expected Reply=0x04) - SwitchAllCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x27, 0x02, 0x25, 0x0b, 0xee
2021-09-16 15:12:44.026 Detail, Node002, Received: 0x01, 0x04, 0x01, 0x13, 0x01, 0xe8
2021-09-16 15:12:44.026 Detail, Node002, ZW_SEND_DATA delivered to Z-Wave stack
2021-09-16 15:12:44.042 Detail, Node002, Received: 0x01, 0x05, 0x00, 0x13, 0x0b, 0x00, 0xe2
2021-09-16 15:12:44.042 Detail, Node002, ZW_SEND_DATA Request with callback ID 0x0b received (expected 0x0b)
2021-09-16 15:12:44.043 Info, Node002, Request RTT 22 Average Request RTT 22
2021-09-16 15:12:44.043 Detail, Expected callbackId was received
2021-09-16 15:12:44.051 Detail, Node002, Received: 0x01, 0x09, 0x00, 0x04, 0x00, 0x02, 0x03, 0x27, 0x03, 0xff, 0x28
2021-09-16 15:12:44.051 Detail,
2021-09-16 15:12:44.051 Info, Node002, Response RTT 31 Average Response RTT 31
2021-09-16 15:12:44.051 Detail, Node002, Initial read of value
2021-09-16 15:12:44.051 Info, Node002, Received SwitchAll report from node 2: On and Off Enabled
2021-09-16 15:12:44.051 Detail, Node002, Expected reply and command class was received
2021-09-16 15:12:44.052 Detail, Node002, Message transaction complete
2021-09-16 15:12:44.052 Detail,
2021-09-16 15:12:44.052 Detail, Node002, Removing current message
2021-09-16 15:12:44.052 Detail, Node002, Notification: ValueChanged
2021-09-16 15:12:44.066 Detail,
2021-09-16 15:12:44.066 Info, Node002, Sending (Query) message (Callback ID=0x0c, Expected Reply=0x04) - PowerlevelCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x73, 0x02, 0x25, 0x0c, 0xbd
2021-09-16 15:12:44.074 Detail, Node002, Received: 0x01, 0x04, 0x01, 0x13, 0x01, 0xe8
2021-09-16 15:12:44.074 Detail, Node002, ZW_SEND_DATA delivered to Z-Wave stack
2021-09-16 15:12:44.089 Detail, Node002, Received: 0x01, 0x05, 0x00, 0x13, 0x0c, 0x00, 0xe5
2021-09-16 15:12:44.089 Detail, Node002, ZW_SEND_DATA Request with callback ID 0x0c received (expected 0x0c)
2021-09-16 15:12:44.089 Info, Node002, Request RTT 22 Average Request RTT 22
2021-09-16 15:12:44.089 Detail, Expected callbackId was received
2021-09-16 15:12:44.098 Detail, Node002, Received: 0x01, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x04, 0x73, 0x03, 0x00, 0x00, 0x87
2021-09-16 15:12:44.098 Detail,
2021-09-16 15:12:44.098 Info, Node002, Response RTT 31 Average Response RTT 31
2021-09-16 15:12:44.098 Info, Node002, Received a PowerLevel report: PowerLevel=Normal, Timeout=0
2021-09-16 15:12:44.098 Detail, Node002, Initial read of value
2021-09-16 15:12:44.098 Detail, Node002, Initial read of value
2021-09-16 15:12:44.099 Detail, Node002, Expected reply and command class was received
2021-09-16 15:12:44.099 Detail, Node002, Message transaction complete
2021-09-16 15:12:44.099 Detail,
2021-09-16 15:12:44.099 Detail, Node002, Removing current message
2021-09-16 15:12:44.099 Detail, Node002, Notification: ValueChanged
2021-09-16 15:12:44.121 Detail, Node002, Notification: ValueChanged
2021-09-16 15:12:44.125 Detail,
2021-09-16 15:12:44.125 Info, Node002, Sending (Query) message (Callback ID=0x0d, Expected Reply=0x04) - ProtectionCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x75, 0x02, 0x25, 0x0d, 0xba
2021-09-16 15:12:44.132 Detail, Node002, Received: 0x01, 0x04, 0x01, 0x13, 0x01, 0xe8
2021-09-16 15:12:44.132 Detail, Node002, ZW_SEND_DATA delivered to Z-Wave stack
2021-09-16 15:12:44.148 Detail, Node002, Received: 0x01, 0x05, 0x00, 0x13, 0x0d, 0x00, 0xe4
2021-09-16 15:12:44.148 Detail, Node002, ZW_SEND_DATA Request with callback ID 0x0d received (expected 0x0d)
2021-09-16 15:12:44.148 Info, Node002, Request RTT 23 Average Request RTT 22
2021-09-16 15:12:44.149 Detail, Expected callbackId was received
2021-09-16 15:12:44.157 Detail, Node002, Received: 0x01, 0x09, 0x00, 0x04, 0x00, 0x02, 0x03, 0x75, 0x03, 0x00, 0x85
2021-09-16 15:12:44.157 Detail,
2021-09-16 15:12:44.157 Info, Node002, Response RTT 32 Average Response RTT 31
2021-09-16 15:12:44.157 Info, Node002, Received a Protection report: Unprotected
2021-09-16 15:12:44.157 Detail, Node002, Initial read of value
2021-09-16 15:12:44.158 Detail, Node002, Expected reply and command class was received
2021-09-16 15:12:44.158 Detail, Node002, Message transaction complete
2021-09-16 15:12:44.158 Detail,
2021-09-16 15:12:44.158 Detail, Node002, Removing current message
2021-09-16 15:12:44.158 Detail, Node002, Notification: ValueChanged
2021-09-16 15:12:44.169 Detail, Node002, Query Stage Complete (Session)
2021-09-16 15:12:44.169 Detail, Node002, AdvanceQueries queryPending=0 queryRetries=0 queryStage=Dynamic live=1
2021-09-16 15:12:44.169 Detail, Node002, QueryStage_Dynamic
2021-09-16 15:12:44.170 Detail, Node002, Queuing (Send) SwitchBinaryCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x25, 0x02, 0x25, 0x0e, 0xe9
2021-09-16 15:12:44.170 Detail, Node002, Queuing (Query) Query Stage Complete (Dynamic)
2021-09-16 15:12:44.170 Detail,
2021-09-16 15:12:44.170 Info, Node002, Sending (Send) message (Callback ID=0x0e, Expected Reply=0x04) - SwitchBinaryCmd_Get (Node=2): 0x01, 0x09, 0x00, 0x13, 0x02, 0x02, 0x25, 0x02, 0x25, 0x0e, 0xe9
2021-09-16 15:12:44.177 Detail, Node002, Received: 0x01, 0x04, 0x01, 0x13, 0x01, 0xe8
2021-09-16 15:12:44.177 Detail, Node002, ZW_SEND_DATA delivered to Z-Wave stack
2021-09-16 15:12:44.192 Detail, Node002, Received: 0x01, 0x05, 0x00, 0x13, 0x0e, 0x00, 0xe7
2021-09-16 15:12:44.193 Detail, Node002, ZW_SEND_DATA Request with callback ID 0x0e received (expected 0x0e)
2021-09-16 15:12:44.193 Info, Node002, Request RTT 22 Average Request RTT 22
2021-09-16 15:12:44.193 Detail, Expected callbackId was received
2021-09-16 15:12:44.201 Detail, Node002, Received: 0x01, 0x09, 0x00, 0x04, 0x00, 0x02, 0x03, 0x25, 0x03, 0x00, 0xd5
2021-09-16 15:12:44.201 Detail,
2021-09-16 15:12:44.202 Info, Node002, Response RTT 31 Average Response RTT 31
2021-09-16 15:12:44.202 Info, Node002, Received SwitchBinary report from node 2: level=Off
2021-09-16 15:12:44.202 Detail, Node002, Initial read of value
2021-09-16 15:12:44.202 Detail, Node002, Expected reply and command class was received
2021-09-16 15:12:44.202 Detail, Node002, Message transaction complete
2021-09-16 15:12:44.202 Detail,
2021-09-16 15:12:44.202 Detail, Node002, Removing current message
2021-09-16 15:12:44.202 Detail, Node002, Notification: ValueChanged
2021-09-16 15:12:44.207 Detail, Node002, Query Stage Complete (Dynamic)
2021-09-16 15:12:44.207 Detail, Node002, AdvanceQueries queryPending=0 queryRetries=0 queryStage=Configuration live=1
2021-09-16 15:12:44.207 Detail, Node002, QueryStage_Configuration
2021-09-16 15:12:44.207 Detail, Node002, QueryStage_Complete
2021-09-16 15:12:44.207 Warning, CheckCompletedNodeQueries m_allNodesQueried=0 m_awakeNodesQueried=0
2021-09-16 15:12:44.208 Warning, CheckCompletedNodeQueries all=1, deadFound=0 sleepingOnly=1
2021-09-16 15:12:44.208 Info, Node query processing complete.
2021-09-16 15:12:44.208 Detail, Node002, Notification: NodeQueriesComplete
2021-09-16 15:12:44.220 Detail, contrlr, Notification: AllNodesQueried
2021-09-16 16:48:44.778 Info, mgr, Manager::WriteConfig completed for driver with home ID of 0xf9bd3698
2021-09-16 16:48:45.783 Info, mgr, Driver for controller /dev/ttyUSB0 pending removal
2021-09-16 16:48:45.784 Detail, Notification: DriverRemoved
2021-09-16 16:48:45.784 Always, ***************************************************************************
2021-09-16 16:48:45.784 Always, ********************* Cumulative Network Statistics *********************
2021-09-16 16:48:45.784 Always, *** General
2021-09-16 16:48:45.784 Always, Driver run time: . . . 0 days, 1 hours, 36 minutes
2021-09-16 16:48:45.784 Always, Frames processed: . . . . . . . . . . . . . . . . . . . . 24
2021-09-16 16:48:45.784 Always, Total messages successfully received: . . . . . . . . . . 24
2021-09-16 16:48:45.784 Always, Total Messages successfully sent: . . . . . . . . . . . . 16
2021-09-16 16:48:45.784 Always, ACKs received from controller: . . . . . . . . . . . . . 16
2021-09-16 16:48:45.784 Always, *** Errors
2021-09-16 16:48:45.784 Always, Unsolicited messages received while waiting for ACK: . . 0
2021-09-16 16:48:45.784 Always, Reads aborted due to timeouts: . . . . . . . . . . . . . 0
2021-09-16 16:48:45.784 Always, Bad checksum errors: . . . . . . . . . . . . . . . . . . 0
2021-09-16 16:48:45.784 Always, CANs received from controller: . . . . . . . . . . . . . 0
2021-09-16 16:48:45.785 Always, NAKs received from controller: . . . . . . . . . . . . . 0
2021-09-16 16:48:45.785 Always, Out of frame data flow errors: . . . . . . . . . . . . . 0
2021-09-16 16:48:45.785 Always, Messages retransmitted: . . . . . . . . . . . . . . . . . 0
2021-09-16 16:48:45.785 Always, Messages dropped and not delivered: . . . . . . . . . . . 0
2021-09-16 16:48:45.785 Always, ***************************************************************************
2021-09-16 16:48:47.796 Info, mgr, Driver for controller /dev/ttyUSB0 removed
2021-09-16 16:48:48.800 Error, mgr, Manager::GetDriver failed - Home ID 0xf9bd3698 is unknown
2021-09-16 16:48:48.800 Warning, Exception: Manager.cpp:373 - 100 - Invalid HomeId passed to GetDriver
2021-09-16 16:48:48.800 Info, mgr, GetSendQueueCount() failed - _homeId -105040232 not found

15
appdaemon/appdaemon.yaml Normal file
View File

@@ -0,0 +1,15 @@
---
secrets: /config/secrets.yaml
appdaemon:
latitude: 52.379189
longitude: 4.899431
elevation: 2
time_zone: Europe/Amsterdam
plugins:
HASS:
type: hass
http:
url: http://127.0.0.1:5050
admin:
api:
hadashboard:

Binary file not shown.

4
appdaemon/apps/apps.yaml Normal file
View File

@@ -0,0 +1,4 @@
---
hello_world:
module: hello
class: HelloWorld

13
appdaemon/apps/hello.py Normal file
View File

@@ -0,0 +1,13 @@
import appdaemon.plugins.hass.hassapi as hass
#
# Hellow World App
#
# Args:
#
class HelloWorld(hass.Hass):
def initialize(self):
self.log("Hello from AppDaemon")
self.log("You are now ready to run Apps!")

View File

@@ -0,0 +1,250 @@
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
padding: 0;
}
body {
margin: 0;
background-color: #222;
font-size: 15px;
color: #fff;
padding: 0;
line-height: 1;
font-family: 'Helvetica Neue', 'Helvetica', 'Open Sans', 'Arial'
}
b, strong {
font-weight: bold;
}
a {
text-decoration: none;
color: inherit;
}
img {
border: 0;
-ms-interpolation-mode: bicubic;
vertical-align: middle;
}
img, object {
max-width: 100%;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;}
iframe {
max-width: 100%;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
}
td {
vertical-align: middle;
}
ul, ol {
padding: 0;
margin: 0;
}
h1, h2, h3, h4, h5, p {
padding: 0;
margin: 0;
}
h1 {
margin-bottom: 6px;
text-align: center;
font-size: 100%;
font-weight: 200;
}
h2 {
font-size: 300%;
font-weight: 400;
color: #fff;
}
h3 {
font-size: 125%;
font-weight: 300;
color: #fff;
}
input {
background-color: #444;
border: none;
}
.dashboard_main {
margin: 0px auto;
}
.gridster {
margin: 0px auto;
}
.icon-background {
pointer-events: none;
width: 100%!important;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0.1;
font-size: 1375%;
text-align: center;
margin-top: 82px;
}
.list-nostyle {
list-style: none;
}
.gridster ul {
list-style: none;
}
.gs-w {
width: 100%;
display: table;
cursor: pointer;
z-index: auto !important;
}
.iframe {
position: relative;
overflow: hidden;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;}
.widget {
padding: 0px 0px;
text-align: center;
width: 100%;
display: table-cell;
vertical-align: middle;
background-color: #444444;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;}
.title {
color: #fff;
}
.icon-inactive {
color: #888;
}
.icon-active {
color: #aaff00;
}
#container {
padding-top: 0px;
}
.modalDialog {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
z-index: 9999;
opacity:0;
-webkit-transition: opacity 400ms ease-in;
-moz-transition: opacity 400ms ease-in;
transition: opacity 400ms ease-in;
pointer-events: none;
}
.modalDialogOpen {
opacity:0.95;
pointer-events: auto;
}
.modalDialogClose {
opacity:0;
pointer-events: none;
}
.modalDialog > div {
width: 275px;
position: relative;
margin: 3% auto;
padding: 5px 20px 13px 20px;
border-radius: 10px;
}
.modalDialogCloseButton {
line-height: 50px;
position: absolute;
right: -25px;
text-align: center;
top: -20px;
width: 50px;
text-decoration: none;
font-weight: bold;
-webkit-border-radius: 25px;
-moz-border-radius: 25px;
border-radius: 25px;
}
.modalDialogCloseButton:hover { background: #444; }
.widget-basedisplay-default-label .unit {
font-size: 225%;
font-weight: 400;
display: inline-block;
vertical-align: top;
margin-left: 5px;
margin-top: 5px;
}
.widget-basedisplay-default-label .value {
display: inline-block;
vertical-align: middle;
}
.widget-basedisplay-default-label .valueunit {
width: 100%;
vertical-align: middle;
}
.widget-basedisplay-default-label .title {
position: absolute;
top: 5px;
width: 100%;
}
.widget-basedisplay-default-label .title2 {
position: absolute;
top: 23px;
width: 100%;
}
.widget-basedisplay-default-label .state_text {
position: absolute;
bottom: -3px;
width: 100%;
}
.widget-baseclock-default-clock-clock.date {
position: absolute;
top: 5px;
width: 100%;
}
.widget-baseclock-default-clock-clock.time {
position: absolute;
top: 45px;
width: 100%;
}

View File

@@ -0,0 +1,324 @@
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
padding: 0;
}
body {
margin: 0;
background-color: #222;
font-size: 15px;
color: #fff;
padding: 0;
line-height: 1;
font-family: 'Helvetica Neue', 'Helvetica', 'Open Sans', 'Arial'
}
b, strong {
font-weight: bold;
}
a {
text-decoration: none;
color: inherit;
}
img {
border: 0;
-ms-interpolation-mode: bicubic;
vertical-align: middle;
}
img, object {
max-width: 100%;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;}
iframe {
max-width: 100%;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
}
td {
vertical-align: middle;
}
ul, ol {
padding: 0;
margin: 0;
}
h1, h2, h3, h4, h5, p {
padding: 0;
margin: 0;
}
h1 {
margin-bottom: 6px;
text-align: center;
font-size: 100%;
font-weight: 200;
}
h2 {
font-size: 300%;
font-weight: 400;
color: #fff;
}
h3 {
font-size: 125%;
font-weight: 300;
color: #fff;
}
input {
background-color: #444;
border: none;
}
.dashboard_main {
margin: 0px auto;
}
.gridster {
margin: 0px auto;
}
.icon-background {
pointer-events: none;
width: 100%!important;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0.1;
font-size: 1375%;
text-align: center;
margin-top: 82px;
}
.list-nostyle {
list-style: none;
}
.gridster ul {
list-style: none;
}
.gs-w {
width: 100%;
display: table;
cursor: pointer;
z-index: auto !important;
}
.iframe {
position: relative;
overflow: hidden;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;}
.widget {
padding: 0px 0px;
text-align: center;
width: 100%;
display: table-cell;
vertical-align: middle;
background-color: #444444;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;}
.title {
color: #fff;
}
.icon-inactive {
color: #888;
}
.icon-active {
color: #aaff00;
}
#container {
padding-top: 0px;
}
.modalDialog {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
z-index: 9999;
opacity:0;
-webkit-transition: opacity 400ms ease-in;
-moz-transition: opacity 400ms ease-in;
transition: opacity 400ms ease-in;
pointer-events: none;
}
.modalDialogOpen {
opacity:0.95;
pointer-events: auto;
}
.modalDialogClose {
opacity:0;
pointer-events: none;
}
.modalDialog > div {
width: 275px;
position: relative;
margin: 3% auto;
padding: 5px 20px 13px 20px;
border-radius: 10px;
}
.modalDialogCloseButton {
line-height: 50px;
position: absolute;
right: -25px;
text-align: center;
top: -20px;
width: 50px;
text-decoration: none;
font-weight: bold;
-webkit-border-radius: 25px;
-moz-border-radius: 25px;
border-radius: 25px;
}
.modalDialogCloseButton:hover { background: #444; }
.widget-baseclock-default-clock-clock.date {
position: absolute;
top: 5px;
width: 100%;
}
.widget-baseclock-default-clock-clock.time {
position: absolute;
top: 45px;
width: 100%;
}
.widget-basedisplay-default-outside-temp .unit {
font-size: 225%;
font-weight: 400;
display: inline-block;
vertical-align: top;
margin-left: 5px;
margin-top: 5px;
}
.widget-basedisplay-default-outside-temp .value {
display: inline-block;
vertical-align: middle;
}
.widget-basedisplay-default-outside-temp .valueunit {
width: 100%;
vertical-align: middle;
}
.widget-basedisplay-default-outside-temp .title {
position: absolute;
top: 5px;
width: 100%;
}
.widget-basedisplay-default-outside-temp .title2 {
position: absolute;
top: 23px;
width: 100%;
}
.widget-basedisplay-default-outside-temp .state_text {
position: absolute;
bottom: -3px;
width: 100%;
}
.widget-baseclimate-default-inside-temp .title {
position: absolute;
top: 5px;
width: 100%;
}
.widget-baseclimate-default-inside-temp .title2 {
position: absolute;
top: 23px;
width: 100%;
}
.widget-baseclimate-default-inside-temp .level {
font-size: 250%;
display: inline-block;
}
.widget-baseclimate-default-inside-temp .units {
font-size: 100%;
font-weight: 400;
display: inline-block;
vertical-align: top;
margin-left: 5px;
margin-top: 5px;
}
.widget-baseclimate-default-inside-temp .levelunits {
position: absolute;
top: 43px;
width: 100%;
}
.widget-baseclimate-default-inside-temp .secondary-icon {
position: absolute;
bottom: 0px;
font-size: 20px;
width: 32px;
color: white;
}
.widget-baseclimate-default-inside-temp .level2 {
display: inline-block;
}
.widget-baseclimate-default-inside-temp .units2 {
font-size: 65%;
font-weight: 400;
display: inline-block;
vertical-align: top;
}
.widget-baseclimate-default-inside-temp .levelunits2 {
position: absolute;
bottom: 5px;
width: 100%;
}
.widget-baseclimate-default-inside-temp .secondary-icon.plus {
right: 24px;
}
.widget-baseclimate-default-inside-temp .secondary-icon.plus i {
padding-top: 10px;
padding-left: 30px;
}
.widget-baseclimate-default-inside-temp .secondary-icon.minus {
left: 8px;
}
.widget-baseclimate-default-inside-temp .secondary-icon.minus i {
padding-top: 10px;
padding-right: 30px;
}

View File

@@ -0,0 +1,6 @@
<! body tags ->
<! body tags go here ->

View File

@@ -0,0 +1,6 @@
<! head tags ->
<! head tags go here ->

View File

@@ -0,0 +1,6 @@
<! body tags ->
<! body tags go here ->

View File

@@ -0,0 +1,6 @@
<! head tags ->
<! head tags go here ->

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,134 @@
var myDeviceID;
$(function(){ //DOM Ready
function navigate(url)
{
window.location.href = url;
}
$(document).attr("title", "Hello Panel");
content_width = (120 + 5) * 8 + 5
$('.gridster').width(content_width)
$(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [120, 120],
avoid_overlapped_widgets: true,
max_rows: 15,
max_size_x: 8,
shift_widgets_up: false
}).data('gridster').disable();
// Add Widgets
var gridster = $(".gridster ul").gridster().data('gridster');
gridster.add_widget('<li><div data-bind="attr: {style: widget_style}" class="widget widget-basedisplay-default-label" id="default-label"><h1 class="title" data-bind="text: title, attr:{ style: title_style}"></h1><h1 class="title2" data-bind="text: title2, attr:{ style: title2_style}"></h1><div class="valueunit" data-bind="attr:{ style: container_style}"><h2 class="value" data-bind="html: value, attr:{ style: value_style}"></h2><p class="unit" data-bind="html: unit, attr:{ style: unit_style}"></p></div><h1 class="state_text" data-bind="text: state_text, attr: {style: state_text_style}"></h1></div></li>', 2, 2, 1, 1)
gridster.add_widget('<li><div data-bind="attr: {style: widget_style}" class="widget widget-baseclock-default-clock-clock" id="default-clock-clock"><h1 class="date"data-bind="text: date, attr: {style: date_style}"></h1><h2 class="time" data-bind="text: time, attr: {style: time_style}"></h2></div></li>', 1, 1, 3, 1)
var widgets = {}
// Initialize Widgets
widgets["default-label"] = new basedisplay("default-label", "", "default", {'widget_type': 'basedisplay', 'fields': {'title': '', 'title2': '', 'value': 'Hello World', 'unit': '', 'state_text': ''}, 'static_css': {'title_style': 'color: #fff;', 'title2_style': 'color: #fff;', 'unit_style': '', 'value_style': 'color: #fff;', 'state_text_style': 'color: #fff;', 'widget_style': 'background-color: #444;', 'container_style': ''}, 'css': {}, 'icons': [], 'static_icons': [], 'namespace': 'default'})
widgets["default-clock-clock"] = new baseclock("default-clock-clock", "", "default", {'widget_type': 'baseclock', 'fields': {'date': '', 'time': ''}, 'static_css': {'date_style': 'color: #fff;', 'time_style': 'color: #aa00ff;', 'widget_style': 'background-color: #444;'}, 'static_icons': [], 'icons': [], 'css': {}, 'entity': 'clock.clock', 'title_is_friendly_name': 1, 'namespace': 'default'})
// Setup click handler to cancel timeout navigations
$( ".gridster" ).click(function(){
clearTimeout(myTimeout);
if (myTimeoutSticky) {
myTimeout = setTimeout(function() { navigate(myTimeoutUrl); }, myTimeoutDelay);
}
});
// Set up timeout
var myTimeout;
var myTimeoutUrl;
var myTimeoutDelay;
var myTimeoutSticky = 0;
if (location.search != "")
{
console.log("begin")
var query = location.search.substr(1);
var result = {};
query.split("&").forEach(function(part) {
var item = part.split("=");
result[item[0]] = decodeURIComponent(item[1]);
});
if ("deviceid" in result)
{
myDeviceID = result.deviceid;
try
{
setCookie('ADdevID', myDeviceID);
}
catch (e)
{
console.log(e);
}
}
else
{
try
{
myDeviceID = getCookie('ADdevID');
}
catch (e)
{
console.log(e);
myDeviceID = null;
}
}
if ("timeout" in result && "return" in result)
{
url = result.return
argcount = 0
for (arg in result)
{
if (arg != "timeout" && arg != "return" && arg != "sticky")
{
if (argcount == 0)
{
url += "?";
}
else
{
url += "&";
}
argcount ++;
url += arg + "=" + result[arg]
}
}
if ("sticky" in result)
{
myTimeoutSticky = (result.sticky == "1");
}
myTimeoutUrl = url;
myTimeoutDelay = result.timeout * 1000;
myTimeout = setTimeout(function() { navigate(url); }, result.timeout * 1000);
}
}
else
{
try
{
myDeviceID = getCookie('ADdevID');
}
catch (e)
{
console.log(e);
myDeviceID = null;
}
}
// Start listening for AD Events
window.dashstream = new DashStream("ws", location.protocol, document.domain, location.port, "Hello Panel", widgets);
});

View File

@@ -0,0 +1,138 @@
var myDeviceID;
$(function(){ //DOM Ready
function navigate(url)
{
window.location.href = url;
}
$(document).attr("title", "Main Panel");
content_width = (120 + 5) * 8 + 5
$('.gridster').width(content_width)
$(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [120, 120],
avoid_overlapped_widgets: true,
max_rows: 15,
max_size_x: 8,
shift_widgets_up: false
}).data('gridster').disable();
// Add Widgets
var gridster = $(".gridster ul").gridster().data('gridster');
gridster.add_widget('<li><div data-bind="attr: {style: widget_style}" class="widget widget-baseclock-default-clock-clock" id="default-clock-clock"><h1 class="date"data-bind="text: date, attr: {style: date_style}"></h1><h2 class="time" data-bind="text: time, attr: {style: time_style}"></h2></div></li>', 2, 2, 1, 1)
gridster.add_widget('<li><div data-bind="attr: {style: widget_style}" class="widget widget-basedisplay-default-outside-temp" id="default-outside-temp"><h1 class="title" data-bind="text: title, attr:{ style: title_style}"></h1><h1 class="title2" data-bind="text: title2, attr:{ style: title2_style}"></h1><div class="valueunit" data-bind="attr:{ style: container_style}"><h2 class="value" data-bind="html: value, attr:{ style: value_style}"></h2><p class="unit" data-bind="html: unit, attr:{ style: unit_style}"></p></div><h1 class="state_text" data-bind="text: state_text, attr: {style: state_text_style}"></h1></div></li>', 1, 1, 3, 1)
gridster.add_widget('<li><div data-bind="attr: {style: widget_style}" class="widget widget-baseclimate-default-inside-temp" id="default-inside-temp"><h1 class="title" data-bind="text: title, attr:{style: title_style}"></h1><h1 class="title2" data-bind="text: title2, attr:{style: title2_style}"></h1><div class="levelunits"><h2 class="level" data-bind="text: level, attr:{ style: level_style}"></h2><p class="units" data-bind="html: units, attr:{ style: unit_style}"></p></div><div class="levelunits2"><p class="level2" data-bind="text: level2, attr:{style: level2_style}"></p><p class="units2" data-bind="html: units, attr:{style: unit2_style}"></p></div><p class="secondary-icon minus"><i data-bind="attr: {class: icon_down, style: level_down_style}" id="level-down"></i></p><p class="secondary-icon plus"><i data-bind="attr: {class: icon_up, style: level_up_style}" id="level-up"></i></p></div></li>', 1, 1, 4, 1)
var widgets = {}
// Initialize Widgets
widgets["default-clock-clock"] = new baseclock("default-clock-clock", "", "default", {'widget_type': 'baseclock', 'fields': {'date': '', 'time': ''}, 'static_css': {'date_style': 'color: #fff;', 'time_style': 'color: #aa00ff;', 'widget_style': 'background-color: #444;'}, 'static_icons': [], 'icons': [], 'css': {}, 'entity': 'clock.clock', 'title_is_friendly_name': 1, 'use_comma': 0, 'precision': 1, 'use_hass_icon': 1, 'namespace': 'default'})
widgets["default-outside-temp"] = new basedisplay("default-outside-temp", "", "default", {'widget_type': 'basedisplay', 'entity': 'sensor.thermostat_outside_temperature', 'entity_to_sub_entity_attribute': '', 'sub_entity': '', 'sub_entity_to_entity_attribute': '', 'fields': {'title': 'Outside Temperature', 'title2': '', 'value': '', 'unit': '', 'state_text': ''}, 'static_css': {'title_style': 'color: #fff;', 'title2_style': 'color: #fff;', 'state_text_style': 'color: #fff;font-size: 100%;', 'widget_style': 'background-color: #444;', 'container_style': ''}, 'css': {'value_style': 'color: #00aaff;font-size: 250%;', 'text_style': 'color: #fff;font-size: 100%;', 'unit_style': 'color: #00aaff;font-size: 100%;'}, 'icons': [], 'static_icons': [], 'units': '&deg;F', 'precision': 1, 'use_comma': 0, 'use_hass_icon': 1, 'namespace': 'default'})
widgets["default-inside-temp"] = new baseclimate("default-inside-temp", "", "default", {'widget_type': 'baseclimate', 'entity': 'climate.home', 'post_service': {'service': 'climate/set_temperature', 'entity_id': 'climate.home'}, 'fields': {'title': 'Thermostat', 'title2': '', 'units': '', 'level': '', 'level2': ''}, 'icons': [], 'css': {}, 'static_icons': {'icon_up': 'fas-plus', 'icon_down': 'fas-minus'}, 'static_css': {'title_style': 'color: #fff;', 'title2_style': 'color: #fff;', 'level_style': 'color: #00aaff;', 'level2_style': 'color: #00aaff;', 'level_up_style': 'color: #888;', 'level_down_style': 'color: #888;', 'widget_style': 'background-color: #444;', 'unit_style': 'color: #00aaff;', 'unit2_style': 'color: #00aaff;'}, 'use_comma': 0, 'precision': 1, 'use_hass_icon': 1, 'namespace': 'default'})
// Setup click handler to cancel timeout navigations
$( ".gridster" ).click(function(){
clearTimeout(myTimeout);
if (myTimeoutSticky) {
myTimeout = setTimeout(function() { navigate(myTimeoutUrl); }, myTimeoutDelay);
}
});
// Set up timeout
var myTimeout;
var myTimeoutUrl;
var myTimeoutDelay;
var myTimeoutSticky = 0;
if (location.search != "")
{
console.log("begin")
var query = location.search.substr(1);
var result = {};
query.split("&").forEach(function(part) {
var item = part.split("=");
result[item[0]] = decodeURIComponent(item[1]);
});
if ("deviceid" in result)
{
myDeviceID = result.deviceid;
try
{
setCookie('ADdevID', myDeviceID);
}
catch (e)
{
console.log(e);
}
}
else
{
try
{
myDeviceID = getCookie('ADdevID');
}
catch (e)
{
console.log(e);
myDeviceID = null;
}
}
if ("timeout" in result && "return" in result)
{
url = result.return
argcount = 0
for (arg in result)
{
if (arg != "timeout" && arg != "return" && arg != "sticky")
{
if (argcount == 0)
{
url += "?";
}
else
{
url += "&";
}
argcount ++;
url += arg + "=" + result[arg]
}
}
if ("sticky" in result)
{
myTimeoutSticky = (result.sticky == "1");
}
myTimeoutUrl = url;
myTimeoutDelay = result.timeout * 1000;
myTimeout = setTimeout(function() { navigate(url); }, result.timeout * 1000);
}
}
else
{
try
{
myDeviceID = getCookie('ADdevID');
}
catch (e)
{
console.log(e);
myDeviceID = null;
}
}
// Start listening for AD Events
window.dashstream = new DashStream("ws", location.protocol, document.domain, location.port, "Main Panel", widgets);
});

View File

@@ -0,0 +1,14 @@
#
# Main arguments, all optional
#
title: Hello Panel
widget_dimensions: [120, 120]
widget_margins: [5, 5]
columns: 8
label:
widget_type: label
text: Hello World
layout:
- label(2x2), clock.clock

View File

@@ -0,0 +1,36 @@
title: Main Panel
widget_dimensions: [120, 120]
widget_size: [1, 1]
widget_margins: [5, 5]
columns: 8
global_parameters:
use_comma: 0
precision: 1
use_hass_icon: 1
namespace: default
layout:
- clock.clock (2x2), outside_temp, inside_temp
# - set_temp, spacer, test
test:
widget_type: label
title: test
outside_temp:
widget_type: sensor
title: Outside Temperature
units: "&deg;F"
precision: 1
entity: sensor.thermostat_outside_temperature
#inside_temp:
# widget_type: sensor
# title: Inside Temperature
# units: "&deg;F"
# precision: 1
# entity: sensor.home_temperature
inside_temp:
widget_type: climate
title: Thermostat
entity: climate.home

View File

@@ -1,252 +0,0 @@
- alias: 'Come home lights on if dark'
trigger:
- platform: state
entity_id: device_tracker.dan_dansphone
to: 'home'
condition:
condition: and
conditions:
- condition: state
entity_id: device_tracker.dan_dansphone
state: 'home'
- condition: sun
after: sunset
after_offset: '-01:00:00'
action:
- service: light.turn_on
entity_id: group.lightautomation
- service: notify.notify_dan
data:
title: 'Lights on'
message: 'All lights turned on'
- delay: '00:05:00'
- service: light.turn_off
entity_id: light.sengled_e11g13_030b3e1d_1
- service: notify.notify_dan
data:
title: 'EntryWay Light Off - Beacon'
message: 'Entryway light turned off. Welcome Home.'
- alias: 'Leave and lights turn off'
trigger:
platform: state
entity_id: device_tracker.dan_dansphone
to: 'not_home'
condition:
condition: state
entity_id: group.all_lights
state: 'on'
action:
- service: light.turn_off
entity_id: group.all_lights
- service: notify.notify_dan
data:
title: 'Lights off'
message: 'All lights have been turned off'
- alias: 'Switch on'
trigger:
platform: sun
event: sunset
offset: '-01:00:00'
action:
service: switch.turn_on
entity_id: switch.linear_ps15z2_plugin_appliance_module_switch
- alias: 'Switch off'
trigger:
platform: time
at: '00:00:00'
action:
service: switch.turn_off
entity_id: switch.linear_ps15z2_plugin_appliance_module_switch
#- alias: 'Lights_On_Beacon'
# trigger:
# - platform: state
# entity_id: sensor.Dan, sensor.Guest
# from: 'not_home'
# condition:
# condition: and
# conditions:
# - condition: sun
# after: sunset
# after_offset: "-1:00:00"
# - condition: state
# entity_id: group.lightautomation
# state: 'off'
# action:
# - service: light.turn_on
# entity_id: group.lightautomation
# - service: notify.notify_dan
# data:
# title: 'Lights on - Beacon'
# message: 'All lights turned on via beacon'
#
#- alias: 'Entryway_Off_Beacon'
# trigger:
# - platform: state
# entity_id: sensor.Dan
# from: 'EntryWay - HB'
# to: 'Bedroom'
# condition:
# condition: state
# entity_id: light.sengled_e11g13_030b3e1d_1
# state: 'on'
# action:
# - service: light.turn_off
# entity_id: light.sengled_e11g13_030b3e1d_1
# - service: notify.notify_dan
# data:
# title: 'EntryWay Light Off - Beacon'
# message: 'Entryway light turned off. Welcome Home.'
#
#- alias: 'Lights_off_Beacon'
# trigger:
# platform: state
# entity_id: sensor.Dan, sensor.Guest
# to: 'not_home'
# condition:
# condition: state
# entity_id: group.all_lights
# state: 'on'
# action:
# - service: light.turn_off
# entity_id: group.all_lights
# - service: notify.notify_dan
# data:
# title: 'Lights off - Beacon'
# message: 'All lights have been turned off via beacon'
- alias: 'Lights on when home'
trigger:
- platform: sun
event: sunset
offset: '-01:00:00'
condition:
condition: and
conditions:
- condition: template
##value_template: "{% if not is_state('sensor.dan', 'not_home') or not is_state('sensor.guest', 'not_home') or is_state('device_tracker.dan_dansphone', 'home') %}true{% endif %}"
value_template: "{% if is_state('device_tracker.dan_dansphone', 'home') %}true{% endif %}"
- condition: state
entity_id: group.lightlivingroom
state: 'off'
action:
- service: light.turn_on
entity_id: group.lightlivingroom
- service: notify.notify_dan
data:
title: 'Living Room Lights on - Sunset'
message: 'Living Room Lights turned on. But you probably know that.'
- alias: 'IFTT'
trigger:
platform: event
event_type: ifttt_webhook_received
event_data:
action: call_service
action:
service_template: '{{ trigger.event.data.service }}'
data_template:
entity_id: '{{ trigger.event.data.entity_id }}'
- alias: 'Sleep - Lights Off'
trigger:
platform: state
entity_id: sensor.sleep_Info
to: 'startsleep'
action:
- service: light.turn_off
entity_id: group.all_lights
- alias: 'Sleep - Lights On'
trigger:
platform: state
entity_id: sensor.sleep_Info
to: 'stopsleep'
action:
- service: light.turn_on
entity_id: group.all_lights
#########Climate Automations######
- alias: 'Set Away Mode'
trigger:
platform: state
entity_id: device_tracker.dan_dansphone
to: 'not_home'
from: 'home'
condition:
condition: template
value_template: "{% if not is_state('climate.home', 'off') %}true{% endif %}"
action:
- service: climate.set_preset_mode
entity_id: climate.home
data:
preset_mode: 'away'
- service: notify.notify_dan
data:
title: 'Left House - Set thermostate to Away'
message: 'Thermostat set to away mode'
- service: input_boolean.turn_on
data:
entity_id: input_boolean.thermostat_mode
- alias: 'Leave Work Set Home Mode'
trigger:
platform: state
entity_id: device_tracker.dan_dansphone
to: 'not_home'
from: 'Work'
condition:
- condition: template
value_template: "{% if not is_state('climate.home', 'off') %}true{% endif %}"
- condition: time
weekday:
- mon
- tue
- wed
- thu
action:
- service: climate.set_preset_mode
entity_id: climate.home
data:
preset_mode: 'home'
- service: notify.notify_dan
data:
title: 'Left Work - Set thermostate to Home'
message: 'Thermostat set to home mode'
- service: input_boolean.turn_off
data:
entity_id: input_boolean.thermostat_mode
- alias: 'Set Home Mode'
trigger:
platform: state
entity_id: device_tracker.dan_dansphone
to: 'home'
from: 'not_home'
condition:
- condition: template
value_template: "{% if not is_state('climate.home', 'off') %}true{% endif %}"
- condition: state
entity_id: input_boolean.thermostat_mode
state: 'on'
action:
- service: climate.set_preset_mode
entity_id: climate.home
data:
preset_mode: 'home'
- service: notify.notify_dan
data:
title: 'Back Home - Set thermostate to Home'
message: 'Thermostat set to home mode'
- service: input_boolean.turn_off
data:
entity_id: input_boolean.thermostat_mode

View File

@@ -0,0 +1,50 @@
blueprint:
name: Motion-activated Light
description: Turn on a light when motion is detected.
domain: automation
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml
input:
motion_entity:
name: Motion Sensor
selector:
entity:
domain: binary_sensor
device_class: motion
light_target:
name: Light
selector:
target:
entity:
domain: light
no_motion_wait:
name: Wait time
description: Time to leave the light on after last motion is detected.
default: 120
selector:
number:
min: 0
max: 3600
unit_of_measurement: seconds
# If motion is detected within the delay,
# we restart the script.
mode: restart
max_exceeded: silent
trigger:
platform: state
entity_id: !input motion_entity
from: "off"
to: "on"
action:
- service: light.turn_on
target: !input light_target
- wait_for_trigger:
platform: state
entity_id: !input motion_entity
from: "on"
to: "off"
- delay: !input no_motion_wait
- service: light.turn_off
target: !input light_target

View File

@@ -0,0 +1,43 @@
blueprint:
name: Zone Notification
description: Send a notification to a device when a person leaves a specific zone.
domain: automation
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
input:
person_entity:
name: Person
selector:
entity:
domain: person
zone_entity:
name: Zone
selector:
entity:
domain: zone
notify_device:
name: Device to notify
description: Device needs to run the official Home Assistant app to receive notifications.
selector:
device:
integration: mobile_app
trigger:
platform: state
entity_id: !input person_entity
variables:
zone_entity: !input zone_entity
# This is the state of the person when it's in this zone.
zone_state: "{{ states[zone_entity].name }}"
person_entity: !input person_entity
person_name: "{{ states[person_entity].name }}"
condition:
condition: template
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
action:
domain: mobile_app
type: notify
device_id: !input notify_device
message: "{{ person_name }} has left {{ zone_state }}"

View File

@@ -0,0 +1,74 @@
blueprint:
name: Confirmable Notification
description: >-
A script that sends an actionable notification with a confirmation before
running the specified action.
domain: script
source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml
input:
notify_device:
name: Device to notify
description: Device needs to run the official Home Assistant app to receive notifications.
selector:
device:
integration: mobile_app
title:
name: "Title"
description: "The title of the button shown in the notification."
default: ""
selector:
text:
message:
name: "Message"
description: "The message body"
selector:
text:
confirm_text:
name: "Confirmation Text"
description: "Text to show on the confirmation button"
default: "Confirm"
selector:
text:
confirm_action:
name: "Confirmation Action"
description: "Action to run when notification is confirmed"
default: []
selector:
action:
dismiss_text:
name: "Dismiss Text"
description: "Text to show on the dismiss button"
default: "Dismiss"
selector:
text:
dismiss_action:
name: "Dismiss Action"
description: "Action to run when notification is dismissed"
default: []
selector:
action:
mode: restart
sequence:
- alias: "Send notification"
domain: mobile_app
type: notify
device_id: !input notify_device
title: !input title
message: !input message
data:
actions:
- action: "CONFIRM"
title: !input confirm_text
- action: "DISMISS"
title: !input dismiss_text
- alias: "Awaiting response"
wait_for_trigger:
- platform: event
event_type: mobile_app_notification_action
- choose:
- conditions: "{{ wait.trigger.event.data.action == 'CONFIRM' }}"
sequence: !input confirm_action
- conditions: "{{ wait.trigger.event.data.action == 'DISMISS' }}"
sequence: !input dismiss_action

View File

@@ -1,12 +0,0 @@
- alias: 'Set Away Mode'
trigger:
platform: state
entity_id: device_tracker.dan_dansphone
to: 'not_home'
condition: template
value_template: "{% if not is_state('thermostat_current_status', 'off') %}true{% endif %}"
action:
- service: set_away_mode
away_mode: on

View File

@@ -1,7 +0,0 @@
- platform: template
sensors:
- thermostat_current_status:
value_template: '{{ states.climate.house.attributes.operation }}'
- thermostat_away_mode:
value_template: '{{ states.climate.house.attributes.away_mode }}'

View File

@@ -1,69 +1,38 @@
homeassistant:
# Name of the location where Home Assistant is running
name: Home
# Location required to calculate the time the sun rises and sets
latitude: 41.3568
longitude: -82.262
# Impacts weather/sunrise data (altitude above sea level in meters)
elevation: 244
# metric for Metric, imperial for Imperial
unit_system: imperial
# Pick yours from here: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
time_zone: America/New_York
# Customization file
customize: !include customize.yaml
# Show links to resources in log and frontend
#introduction:
# Enables the frontend
frontend: frontend:
lovelace: lovelace:
mode: yaml mode: yaml
# Enables configuration UI
config:
recorder: recorder:
db_url: postgresql://ha:password@docker.dandev.us/homeassistant db_url: mysql://house:x3147qwfsbb9ctWy@192.168.86.198/house?charset=utf8
purge_interval: 0 #purge_interval: 0
auto_purge: false
history:
# log:
system_health:
mobile_app:
# hassio:
map:
#namecheapdns: # Configure a default setup of Home Assistant (frontend, api, etc)
# domain: danshouse.space config:
# password: c6dc6147fab242a2a4c50688790995c5 default_config:
http: http:
# Secrets are defined in the file secrets.yaml use_x_forwarded_for: true
#api_password: ICYN30blah trusted_proxies:
# Uncomment this if you are using SSL/TLS, running in Docker container, etc. - 192.168.86.198
alexa:
base_url: icyn30.duckdns.org:8123 # Text to speech
ssl_certificate: /ssl/fullchain.pem tts:
ssl_key: /ssl/privkey.pem - platform: google_translate
# Checks for available updates
# Note: This component will send some information about your system to
# the developers to assist with development of Home Assistant.
# For more information, please see:
# https://home-assistant.io/blog/2016/10/25/explaining-the-updater/
updater:
# Optional, allows Home Assistant developers to focus on popular components.
# include_used_components: true
# Discover some devices automatically group: !include groups.yaml
#discovery: automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
# Allows you to issue voice commands from the frontend in enabled browsers
conversation:
# Enables support for tracking state changes over time
history:
# View all events in a logbook
logbook:
# Track the sun
sun:
#weather #weather
weather: weather:
@@ -72,46 +41,21 @@ weather:
latitude: 41.469249 latitude: 41.469249
longitude: -81.713146 longitude: -81.713146
name: Amherst name: Amherst
sun:
hue:
bridges:
- host: 192.168.86.44
allow_unreachable: true
# sensors # sensors
sensor: sensor:
- platform: yr #- platform: yr
# - platform: darksky - platform: sonarr
# api_key: 19b4b95365ea88c65b085805ef046ba0 api_key: 0db45f76dbfc432ab5c86d8346d5165e
# latitude: 41.469249 host: 192.168.86.198:8989
# longitude: -81.713146 days: 7
# monitored_conditions: # climate template sensors
# - icon
# - precip_type
# - precip_intensity
# - precip_probability
# - apparent_temperature
# - apparent_temperature_max
# - apparent_temperature_min
# - precip_intensity_max
# - hourly_summary
# - platform: mqtt
# state_topic: "owntracks/HA/DansPhone"
# name: "Phone Battery"
# unit_of_measurement: "%"
# value_template: '{{ value_json.batt }}'
# - platform: mqtt_room
# device_id: e2c56db5dffb48d2b060d0f5a71096e0_0000_0000
# name: 'Dan'
# state_topic: 'happy-bubbles/presence/ha'
# away_timeout: 60
# - platform: mqtt_room
# device_id: 74278bdab64445208f0c720eaf059935_0000_0000
# name: 'Guest'
# state_topic: 'happy-bubbles/presence/ha'
# away_timeout: 60
# - platform: sonarr
# api_key: c544cae167164f11947c84ad4e04f61c
# host: 192.168.86.20
# monitored_conditions:
# - upcoming
# - diskspace
# days: 7
- platform: template - platform: template
sensors: sensors:
thermostat_current_status: thermostat_current_status:
@@ -128,83 +72,33 @@ sensor:
sensors: sensors:
thermostat_outside_temperature: thermostat_outside_temperature:
unit_of_measurement: "°F" unit_of_measurement: "°F"
value_template: "{{ float(states.weather.home.attributes.temperature) }}" value_template: "{{ float(states.weather.home_2.attributes.temperature) }}"
#name: "Outside Temperature"
#MQTT sensors
- platform: mqtt - platform: mqtt
state_topic: "upstairstemp/sensor/temp" state_topic: "house/sensor/bedroom_temperature/state"
name: "Upstairs Temperature" name: "Upstairs Temperature"
unit_of_measurement: "°F" unit_of_measurement: "°F"
- platform: mqtt
state_topic: "house/sensor/bedroom_humidity/state"
name: "Upstairs Humidity"
unit_of_measurement: "%"
- platform: mqtt
state_topic: "house/sensor/living_room_brightness/state"
name: "Living Room Brightness"
- platform: mqtt - platform: mqtt
state_topic: "DansPhone/sleepinfo" state_topic: "DansPhone/sleepinfo"
name: "sleep Info" name: "sleep Info"
#Alexa Intents
intent_script:
ActivateScene:
action:
service: scene.turn_on
target:
entity_id: scene.{{ Scene | replace(" ", "_") }}
speech:
type: plain
text: OK
# Text to speech
#tts:
# - platform: google
#mqtt:
# broker: core-mosquitto
# username: HA
# password: login
#device_tracker:
# - platform: owntracks
ifttt:
key: bO6GhKW9xCaQoIigKnq1qF
telegram_bot:
- platform: broadcast
api_key: 374239722:AAGd4y_LsEoOe5IUUblKXehCQFJV1s2JdQo
allowed_chat_ids:
438758485
notify:
name: notify_dan
platform: telegram
chat_id: 438758485
hue:
bridges:
- host: 192.168.86.44
allow_unreachable: true
alexa:
zwave:
usb_path: /dev/ttyUSB0
zha:
usb_path: /dev/ttyUSB1
database_path: /config/zigbee.db
ecobee:
api_key: 8DSRpsILZJNUIJXvSzY6C4ufrMQNQsHa
input_boolean:
thermostat_mode:
name: Thermostat Away Mode
influxdb:
host: docker.dandev.us
include:
entities:
- sensor.home_temperature
- sensor.home_humidity
- sensor.thermostat_outside_temperature
- sensor.upstairs_temperature
- device_tracker.dan_dansphone
- sensor.thermostate_current_status
- sensor.thermostate_climate_mode
- light.sengled_e11g13_030b22ba_1
- light.sengled_e11g13_030b3e1d_1
- light.sengled_e11g13_030b3efd_1
- light.sengled_e11g13_030b40af_1
- light.sengled_e11g13_031468d6_1
- light.table
group: !include groups.yaml
automation: !include automations.yaml
script: !include scripts.yaml
zone: !include zones.yaml
intent_script: !include intents.yaml

View File

@@ -1,12 +0,0 @@
>
{{ [
"OK",
"Sure",
"I gotchu",
"'ight",
"Done",
"No worries",
"Ain't no thang",
"As you wish",
"No problem"
] | random }}

View File

@@ -0,0 +1,68 @@
"""
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
https://hacs.xyz/
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
import voluptuous as vol
from .const import DOMAIN, PLATFORMS
from .enums import HacsDisabledReason
from .helpers.functions.configuration_schema import hacs_config_combined
from .operational.setup import (
async_setup as hacs_yaml_setup,
async_setup_entry as hacs_ui_setup,
)
if TYPE_CHECKING:
from .base import HacsBase
CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up this integration using yaml."""
return await hacs_yaml_setup(hass, config)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
config_entry.add_update_listener(async_reload_entry)
return await hacs_ui_setup(hass, config_entry)
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
hacs: HacsBase = hass.data[DOMAIN]
for task in hacs.recuring_tasks:
# Cancel all pending tasks
task()
try:
if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel")
hass.components.frontend.async_remove_panel("hacs")
except AttributeError:
pass
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
hacs.disable_hacs(HacsDisabledReason.REMOVED)
hass.data.pop(DOMAIN, None)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Reload the HACS config entry."""
await async_unload_entry(hass, config_entry)
await async_setup_entry(hass, config_entry)

View File

@@ -0,0 +1 @@
"""Initialize HACS API"""

View File

@@ -0,0 +1,25 @@
"""API Handler for acknowledge_critical_repository"""
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.hacs.helpers.functions.store import (
async_load_from_store,
async_save_to_store,
)
@websocket_api.async_response
@websocket_api.websocket_command(
{vol.Required("type"): "hacs/critical", vol.Optional("repository"): cv.string}
)
async def acknowledge_critical_repository(hass, connection, msg):
"""Handle get media player cover command."""
repository = msg["repository"]
critical = await async_load_from_store(hass, "critical")
for repo in critical:
if repository == repo["repository"]:
repo["acknowledged"] = True
await async_save_to_store(hass, "critical", critical)
connection.send_message(websocket_api.result_message(msg["id"], critical))

View File

@@ -0,0 +1,24 @@
"""API Handler for check_local_path"""
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist
@websocket_api.async_response
@websocket_api.websocket_command(
{vol.Required("type"): "hacs/check_path", vol.Optional("path"): cv.string}
)
async def check_local_path(_hass, connection, msg):
"""Handle get media player cover command."""
path = msg.get("path")
exist = {"exist": False}
if path is None:
return
if await async_path_exsist(path):
exist["exist"] = True
connection.send_message(websocket_api.result_message(msg["id"], exist))

View File

@@ -0,0 +1,15 @@
"""API Handler for get_critical_repositories"""
from homeassistant.components import websocket_api
import voluptuous as vol
from custom_components.hacs.helpers.functions.store import async_load_from_store
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "hacs/get_critical"})
async def get_critical_repositories(hass, connection, msg):
"""Handle get media player cover command."""
critical = await async_load_from_store(hass, "critical")
if not critical:
critical = []
connection.send_message(websocket_api.result_message(msg["id"], critical))

View File

@@ -0,0 +1,28 @@
"""API Handler for hacs_config"""
from homeassistant.components import websocket_api
import voluptuous as vol
from custom_components.hacs.share import get_hacs
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "hacs/config"})
async def hacs_config(_hass, connection, msg):
"""Handle get media player cover command."""
hacs = get_hacs()
config = hacs.configuration
content = {}
content["frontend_mode"] = config.frontend_mode
content["frontend_compact"] = config.frontend_compact
content["onboarding_done"] = config.onboarding_done
content["version"] = hacs.version
content["frontend_expected"] = hacs.frontend.version_expected
content["frontend_running"] = hacs.frontend.version_running
content["dev"] = config.dev
content["debug"] = config.debug
content["country"] = config.country
content["experimental"] = config.experimental
content["categories"] = hacs.common.categories
connection.send_message(websocket_api.result_message(msg["id"], content))

View File

@@ -0,0 +1,15 @@
"""API Handler for hacs_removed"""
from homeassistant.components import websocket_api
import voluptuous as vol
from custom_components.hacs.share import list_removed_repositories
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "hacs/removed"})
async def hacs_removed(_hass, connection, msg):
"""Get information about removed repositories."""
content = []
for repo in list_removed_repositories():
content.append(repo.to_json())
connection.send_message(websocket_api.result_message(msg["id"], content))

View File

@@ -0,0 +1,65 @@
"""API Handler for hacs_repositories"""
from homeassistant.components import websocket_api
import voluptuous as vol
from custom_components.hacs.share import get_hacs
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"})
async def hacs_repositories(_hass, connection, msg):
"""Handle get media player cover command."""
hacs = get_hacs()
repositories = hacs.repositories
content = []
for repo in repositories:
if (
repo.data.category in hacs.common.categories
and not repo.ignored_by_country_configuration
):
data = {
"additional_info": repo.information.additional_info,
"authors": repo.data.authors,
"available_version": repo.display_available_version,
"beta": repo.data.show_beta,
"can_install": repo.can_install,
"category": repo.data.category,
"country": repo.data.country,
"config_flow": repo.data.config_flow,
"custom": repo.custom,
"default_branch": repo.data.default_branch,
"description": repo.data.description,
"domain": repo.data.domain,
"downloads": repo.data.downloads,
"file_name": repo.data.file_name,
"first_install": repo.status.first_install,
"full_name": repo.data.full_name,
"hide": repo.data.hide,
"hide_default_branch": repo.data.hide_default_branch,
"homeassistant": repo.data.homeassistant,
"id": repo.data.id,
"info": repo.information.info,
"installed_version": repo.display_installed_version,
"installed": repo.data.installed,
"issues": repo.data.open_issues,
"javascript_type": repo.information.javascript_type,
"last_updated": repo.data.last_updated,
"local_path": repo.content.path.local,
"main_action": repo.main_action,
"name": repo.display_name,
"new": repo.data.new,
"pending_upgrade": repo.pending_upgrade,
"releases": repo.data.published_tags,
"selected_tag": repo.data.selected_tag,
"stars": repo.data.stargazers_count,
"state": repo.state,
"status_description": repo.display_status_description,
"status": repo.display_status,
"topics": repo.data.topics,
"updated_info": repo.status.updated_info,
"version_or_commit": repo.display_version_or_commit,
}
content.append(data)
connection.send_message(websocket_api.result_message(msg["id"], content))

View File

@@ -0,0 +1,113 @@
"""API Handler for hacs_repository"""
from aiogithubapi import AIOGitHubAPIException
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.hacs.share import get_hacs
from custom_components.hacs.utils.logger import getLogger
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repository",
vol.Optional("action"): cv.string,
vol.Optional("repository"): cv.string,
}
)
async def hacs_repository(hass, connection, msg):
"""Handle get media player cover command."""
hacs = get_hacs()
logger = getLogger()
data = {}
repository = None
repo_id = msg.get("repository")
action = msg.get("action")
if repo_id is None or action is None:
return
try:
repository = hacs.get_by_id(repo_id)
logger.debug(f"Running {action} for {repository.data.full_name}")
if action == "update":
await repository.update_repository(ignore_issues=True, force=True)
repository.status.updated_info = True
elif action == "install":
repository.data.new = False
was_installed = repository.data.installed
await repository.async_install()
if not was_installed:
hass.bus.async_fire("hacs/reload", {"force": True})
elif action == "not_new":
repository.data.new = False
elif action == "uninstall":
repository.data.new = False
await repository.update_repository(True)
await repository.uninstall()
elif action == "hide":
repository.data.hide = True
elif action == "unhide":
repository.data.hide = False
elif action == "show_beta":
repository.data.show_beta = True
await repository.update_repository()
elif action == "hide_beta":
repository.data.show_beta = False
await repository.update_repository()
elif action == "toggle_beta":
repository.data.show_beta = not repository.data.show_beta
await repository.update_repository()
elif action == "delete":
repository.data.show_beta = False
repository.remove()
elif action == "release_notes":
data = [
{
"name": x.attributes["name"],
"body": x.attributes["body"],
"tag": x.attributes["tag_name"],
}
for x in repository.releases.objects
]
elif action == "set_version":
if msg["version"] == repository.data.default_branch:
repository.data.selected_tag = None
else:
repository.data.selected_tag = msg["version"]
await repository.update_repository()
hass.bus.async_fire("hacs/reload", {"force": True})
else:
logger.error(f"WS action '{action}' is not valid")
await hacs.data.async_write()
message = None
except AIOGitHubAPIException as exception:
message = exception
except AttributeError as exception:
message = f"Could not use repository with ID {repo_id} ({exception})"
except (Exception, BaseException) as exception: # pylint: disable=broad-except
message = exception
if message is not None:
logger.error(message)
hass.bus.async_fire("hacs/error", {"message": str(message)})
if repository:
repository.state = None
connection.send_message(websocket_api.result_message(msg["id"], data))

View File

@@ -0,0 +1,121 @@
"""API Handler for hacs_repository_data"""
import sys
from aiogithubapi import AIOGitHubAPIException
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.hacs.exceptions import HacsException
from custom_components.hacs.helpers.functions.misc import extract_repository_from_url
from custom_components.hacs.helpers.functions.register_repository import (
register_repository,
)
from custom_components.hacs.share import get_hacs
from custom_components.hacs.utils.logger import getLogger
_LOGGER = getLogger()
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repository/data",
vol.Optional("action"): cv.string,
vol.Optional("repository"): cv.string,
vol.Optional("data"): cv.string,
}
)
async def hacs_repository_data(hass, connection, msg):
"""Handle get media player cover command."""
hacs = get_hacs()
repo_id = msg.get("repository")
action = msg.get("action")
data = msg.get("data")
if repo_id is None:
return
if action == "add":
repo_id = extract_repository_from_url(repo_id)
if repo_id is None:
return
if repo_id in hacs.common.skip:
hacs.common.skip.remove(repo_id)
if not hacs.get_by_name(repo_id):
try:
registration = await register_repository(repo_id, data.lower())
if registration is not None:
raise HacsException(registration)
except (
Exception,
BaseException,
) as exception: # pylint: disable=broad-except
hass.bus.async_fire(
"hacs/error",
{
"action": "add_repository",
"exception": str(sys.exc_info()[0].__name__),
"message": str(exception),
},
)
else:
hass.bus.async_fire(
"hacs/error",
{
"action": "add_repository",
"message": f"Repository '{repo_id}' exists in the store.",
},
)
repository = hacs.get_by_name(repo_id)
else:
repository = hacs.get_by_id(repo_id)
if repository is None:
hass.bus.async_fire("hacs/repository", {})
return
_LOGGER.debug("Running %s for %s", action, repository.data.full_name)
try:
if action == "set_state":
repository.state = data
elif action == "set_version":
repository.data.selected_tag = data
await repository.update_repository()
repository.state = None
elif action == "install":
was_installed = repository.data.installed
repository.data.selected_tag = data
await repository.update_repository()
await repository.async_install()
repository.state = None
if not was_installed:
hass.bus.async_fire("hacs/reload", {"force": True})
elif action == "add":
repository.state = None
else:
repository.state = None
_LOGGER.error("WS action '%s' is not valid", action)
message = None
except AIOGitHubAPIException as exception:
message = exception
except AttributeError as exception:
message = f"Could not use repository with ID {repo_id} ({exception})"
except (Exception, BaseException) as exception: # pylint: disable=broad-except
message = exception
if message is not None:
_LOGGER.error(message)
hass.bus.async_fire("hacs/error", {"message": str(message)})
await hacs.data.async_write()
connection.send_message(websocket_api.result_message(msg["id"], {}))

View File

@@ -0,0 +1,54 @@
"""API Handler for hacs_settings"""
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.hacs.share import get_hacs
from custom_components.hacs.utils.logger import getLogger
_LOGGER = getLogger()
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/settings",
vol.Optional("action"): cv.string,
vol.Optional("categories"): cv.ensure_list,
}
)
async def hacs_settings(hass, connection, msg):
"""Handle get media player cover command."""
hacs = get_hacs()
action = msg["action"]
_LOGGER.debug("WS action '%s'", action)
if action == "set_fe_grid":
hacs.configuration.frontend_mode = "Grid"
elif action == "onboarding_done":
hacs.configuration.onboarding_done = True
elif action == "set_fe_table":
hacs.configuration.frontend_mode = "Table"
elif action == "set_fe_compact_true":
hacs.configuration.frontend_compact = False
elif action == "set_fe_compact_false":
hacs.configuration.frontend_compact = True
elif action == "clear_new":
for repo in hacs.repositories:
if repo.data.new and repo.data.category in msg.get("categories", []):
_LOGGER.debug(
"Clearing new flag from '%s'",
repo.data.full_name,
)
repo.data.new = False
else:
_LOGGER.error("WS action '%s' is not valid", action)
hass.bus.async_fire("hacs/config", {})
await hacs.data.async_write()
connection.send_message(websocket_api.result_message(msg["id"], {}))

View File

@@ -0,0 +1,24 @@
"""API Handler for hacs_status"""
from homeassistant.components import websocket_api
import voluptuous as vol
from custom_components.hacs.share import get_hacs
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "hacs/status"})
async def hacs_status(_hass, connection, msg):
"""Handle get media player cover command."""
hacs = get_hacs()
content = {
"startup": hacs.status.startup,
"background_task": hacs.status.background_task,
"lovelace_mode": hacs.core.lovelace_mode,
"reloading_data": hacs.status.reloading_data,
"upgrading_all": hacs.status.upgrading_all,
"disabled": hacs.system.disabled,
"disabled_reason": hacs.system.disabled_reason,
"has_pending_tasks": hacs.queue.has_pending_tasks,
"stage": hacs.stage,
}
connection.send_message(websocket_api.result_message(msg["id"], content))

View File

@@ -0,0 +1,236 @@
"""Base HACS class."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
import json
import logging
import math
import pathlib
from typing import TYPE_CHECKING, Any
from aiogithubapi import (
GitHub,
GitHubAPI,
GitHubAuthenticationException,
GitHubRatelimitException,
)
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession
from homeassistant.core import HomeAssistant
from homeassistant.loader import Integration
from queueman.manager import QueueManager
from .const import REPOSITORY_HACS_DEFAULT
from .enums import (
ConfigurationType,
HacsCategory,
HacsDisabledReason,
HacsStage,
LovelaceMode,
)
from .exceptions import HacsException
from .utils.decode import decode_content
from .utils.logger import getLogger
if TYPE_CHECKING:
from .hacsbase.data import HacsData
from .helpers.classes.repository import HacsRepository
from .operational.factory import HacsTaskFactory
from .tasks.manager import HacsTaskManager
@dataclass
class HacsConfiguration:
"""HacsConfiguration class."""
appdaemon_path: str = "appdaemon/apps/"
appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict)
config_entry: dict[str, str] = field(default_factory=dict)
config_type: ConfigurationType | None = None
country: str = "ALL"
debug: bool = False
dev: bool = False
experimental: bool = False
frontend_compact: bool = False
frontend_mode: str = "Grid"
frontend_repo_url: str = ""
frontend_repo: str = ""
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
onboarding_done: bool = False
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
release_limit: int = 5
sidepanel_icon: str = "hacs:hacs"
sidepanel_title: str = "HACS"
theme_path: str = "themes/"
theme: bool = False
token: str = None
def to_json(self) -> str:
"""Return a json string."""
return asdict(self)
def update_from_dict(self, data: dict) -> None:
"""Set attributes from dicts."""
if not isinstance(data, dict):
raise HacsException("Configuration is not valid.")
for key in data:
self.__setattr__(key, data[key])
@dataclass
class HacsFrontend:
"""HacsFrontend."""
version_running: str | None = None
version_available: str | None = None
version_expected: str | None = None
update_pending: bool = False
@dataclass
class HacsCore:
"""HACS Core info."""
config_path: pathlib.Path | None = None
ha_version: str | None = None
lovelace_mode = LovelaceMode("yaml")
@dataclass
class HacsCommon:
"""Common for HACS."""
categories: set[str] = field(default_factory=set)
default: list[str] = field(default_factory=list)
installed: list[str] = field(default_factory=list)
renamed_repositories: dict[str, str] = field(default_factory=dict)
archived_repositories: list[str] = field(default_factory=list)
skip: list[str] = field(default_factory=list)
@dataclass
class HacsStatus:
"""HacsStatus."""
startup: bool = True
new: bool = False
background_task: bool = False
reloading_data: bool = False
upgrading_all: bool = False
@dataclass
class HacsSystem:
"""HACS System info."""
disabled_reason: HacsDisabledReason | None = None
running: bool = False
stage = HacsStage.SETUP
action: bool = False
@property
def disabled(self) -> bool:
"""Return if HACS is disabled."""
return self.disabled_reason is not None
class HacsBase:
"""Base HACS class."""
_repositories = []
_repositories_by_full_name = {}
_repositories_by_id = {}
common = HacsCommon()
configuration = HacsConfiguration()
core = HacsCore()
data: HacsData | None = None
factory: HacsTaskFactory | None = None
frontend = HacsFrontend()
github: GitHub | None = None
githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None
integration: Integration | None = None
log: logging.Logger = getLogger()
queue: QueueManager | None = None
recuring_tasks = []
repositories: list[HacsRepository] = []
repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None
stage: HacsStage | None = None
status = HacsStatus()
system = HacsSystem()
tasks: HacsTaskManager | None = None
version: str | None = None
@property
def integration_dir(self) -> pathlib.Path:
"""Return the HACS integration dir."""
return self.integration.file_path
async def async_set_stage(self, stage: HacsStage | None) -> None:
"""Set HACS stage."""
if stage and self.stage == stage:
return
self.stage = stage
if stage is not None:
self.log.info("Stage changed: %s", self.stage)
self.hass.bus.async_fire("hacs/stage", {"stage": self.stage})
await self.tasks.async_execute_runtume_tasks()
def disable_hacs(self, reason: HacsDisabledReason) -> None:
"""Disable HACS."""
self.system.disabled_reason = reason
if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason)
def enable_hacs(self) -> None:
"""Enable HACS."""
self.system.disabled_reason = None
self.log.info("HACS is enabled")
def enable_hacs_category(self, category: HacsCategory):
"""Enable HACS category."""
if category not in self.common.categories:
self.log.info("Enable category: %s", category)
self.common.categories.add(category)
def disable_hacs_category(self, category: HacsCategory):
"""Disable HACS category."""
if category in self.common.categories:
self.log.info("Disabling category: %s", category)
self.common.categories.pop(category)
async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for."""
try:
response = await self.githubapi.rate_limit()
if ((limit := response.data.resources.core.remaining or 0) - 1000) >= 15:
return math.floor((limit - 1000) / 15)
self.log.error(
"GitHub API ratelimited - %s remaining", response.data.resources.core.remaining
)
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except GitHubAuthenticationException as exception:
self.log.error("GitHub authentication failed - %s", exception)
self.disable_hacs(HacsDisabledReason.INVALID_TOKEN)
except GitHubRatelimitException as exception:
self.log.error("GitHub API ratelimited - %s", exception)
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except BaseException as exception: # pylint: disable=broad-except
self.log.exception(exception)
return 0
async def async_github_get_hacs_default_file(self, filename: str) -> dict[str, Any]:
"""Get the content of a default file."""
response = await self.githubapi.repos.contents.get(
repository=REPOSITORY_HACS_DEFAULT, path=filename
)
return json.loads(decode_content(response.data.content))

View File

@@ -0,0 +1,157 @@
"""Adds config flow for HACS."""
from aiogithubapi import GitHubDeviceAPI, GitHubException
from aiogithubapi.common.const import OAUTH_USER_LOGIN
from awesomeversion import AwesomeVersion
from homeassistant import config_entries
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_call_later
from homeassistant.loader import async_get_integration
import voluptuous as vol
from custom_components.hacs.const import CLIENT_ID, DOMAIN, MINIMUM_HA_VERSION
from custom_components.hacs.enums import ConfigurationType
from custom_components.hacs.helpers.functions.configuration_schema import (
RELEASE_LIMIT,
hacs_config_option_schema,
)
from custom_components.hacs.mixin import HacsMixin
class HacsFlowHandler(HacsMixin, config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for HACS."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize."""
self._errors = {}
self.device = None
self.activation = None
self._progress_task = None
self._login_device = None
async def async_step_user(self, user_input):
"""Handle a flow initialized by the user."""
self._errors = {}
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input:
if [x for x in user_input if not user_input[x]]:
self._errors["base"] = "acc"
return await self._show_config_form(user_input)
return await self.async_step_device(user_input)
## Initial form
return await self._show_config_form(user_input)
async def async_step_device(self, _user_input):
"""Handle device steps"""
async def _wait_for_activation(_=None):
if self._login_device is None or self._login_device.expires_in is None:
async_call_later(self.hass, 1, _wait_for_activation)
return
response = await self.device.activation(device_code=self._login_device.device_code)
self.activation = response.data
self.hass.async_create_task(
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:
self.device = GitHubDeviceAPI(
client_id=CLIENT_ID,
session=aiohttp_client.async_get_clientsession(self.hass),
**{"client_name": f"HACS/{integration.version}"},
)
async_call_later(self.hass, 1, _wait_for_activation)
try:
response = await self.device.register()
self._login_device = 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:
self.hacs.log.error(exception)
return self.async_abort(reason="github")
return self.async_show_progress_done(next_step_id="device_done")
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
if not user_input:
user_input = {}
if AwesomeVersion(HAVERSION) < MINIMUM_HA_VERSION:
return self.async_abort(
reason="min_ha_version",
description_placeholders={"version": MINIMUM_HA_VERSION},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required("acc_logs", default=user_input.get("acc_logs", False)): bool,
vol.Required("acc_addons", default=user_input.get("acc_addons", False)): bool,
vol.Required(
"acc_untested", default=user_input.get("acc_untested", False)
): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
}
),
errors=self._errors,
)
async def async_step_device_done(self, _user_input):
"""Handle device steps"""
return self.async_create_entry(title="", data={"token": self.activation.access_token})
@staticmethod
@callback
def async_get_options_flow(config_entry):
return HacsOptionsFlowHandler(config_entry)
class HacsOptionsFlowHandler(HacsMixin, config_entries.OptionsFlow):
"""HACS config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
async def async_step_init(self, _user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
limit = int(user_input.get(RELEASE_LIMIT, 5))
if limit <= 0 or limit > 100:
return self.async_abort(reason="release_limit_value")
return self.async_create_entry(title="", data=user_input)
if self.hacs.configuration is None:
return self.async_abort(reason="not_setup")
if self.hacs.configuration.config_type == ConfigurationType.YAML:
schema = {vol.Optional("not_in_use", default=""): str}
else:
schema = hacs_config_option_schema(self.config_entry.options)
del schema["frontend_repo"]
del schema["frontend_repo_url"]
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))

View File

@@ -0,0 +1,302 @@
"""Constants for HACS"""
from aiogithubapi.common.const import ACCEPT_HEADERS
NAME_LONG = "HACS (Home Assistant Community Store)"
NAME_SHORT = "HACS"
DOMAIN = "hacs"
CLIENT_ID = "395a8e669c5de9f7c6e8"
MINIMUM_HA_VERSION = "2021.2.0"
PROJECT_URL = "https://github.com/hacs/integration/"
ISSUE_URL = f"{PROJECT_URL}issues"
DOMAIN_DATA = f"{NAME_SHORT.lower()}_data"
PACKAGE_NAME = "custom_components.hacs"
REPOSITORY_HACS_DEFAULT = "hacs/default"
REPOSITORY_HACS_INTEGRATION = "hacs/integration"
PLATFORMS = ["sensor"]
HACS_ACTION_GITHUB_API_HEADERS = {
"User-Agent": "HACS/action",
"Accept": ACCEPT_HEADERS["preview"],
}
IFRAME = {
"title": "HACS",
"icon": "hacs:hacs",
"url": "/community_overview",
"path": "community",
"require_admin": True,
}
VERSION_STORAGE = "6"
STORENAME = "hacs"
# Messages
NO_ELEMENTS = "No elements to show, open the store to install some awesome stuff."
STARTUP = """
-------------------------------------------------------------------
HACS (Home Assistant Community Store)
Version: {version}
This is a custom integration
If you have any issues with this you need to open an issue here:
https://github.com/hacs/integration/issues
-------------------------------------------------------------------
"""
LOCALE = [
"ALL",
"AF",
"AL",
"DZ",
"AS",
"AD",
"AO",
"AI",
"AQ",
"AG",
"AR",
"AM",
"AW",
"AU",
"AT",
"AZ",
"BS",
"BH",
"BD",
"BB",
"BY",
"BE",
"BZ",
"BJ",
"BM",
"BT",
"BO",
"BQ",
"BA",
"BW",
"BV",
"BR",
"IO",
"BN",
"BG",
"BF",
"BI",
"KH",
"CM",
"CA",
"CV",
"KY",
"CF",
"TD",
"CL",
"CN",
"CX",
"CC",
"CO",
"KM",
"CG",
"CD",
"CK",
"CR",
"HR",
"CU",
"CW",
"CY",
"CZ",
"CI",
"DK",
"DJ",
"DM",
"DO",
"EC",
"EG",
"SV",
"GQ",
"ER",
"EE",
"ET",
"FK",
"FO",
"FJ",
"FI",
"FR",
"GF",
"PF",
"TF",
"GA",
"GM",
"GE",
"DE",
"GH",
"GI",
"GR",
"GL",
"GD",
"GP",
"GU",
"GT",
"GG",
"GN",
"GW",
"GY",
"HT",
"HM",
"VA",
"HN",
"HK",
"HU",
"IS",
"IN",
"ID",
"IR",
"IQ",
"IE",
"IM",
"IL",
"IT",
"JM",
"JP",
"JE",
"JO",
"KZ",
"KE",
"KI",
"KP",
"KR",
"KW",
"KG",
"LA",
"LV",
"LB",
"LS",
"LR",
"LY",
"LI",
"LT",
"LU",
"MO",
"MK",
"MG",
"MW",
"MY",
"MV",
"ML",
"MT",
"MH",
"MQ",
"MR",
"MU",
"YT",
"MX",
"FM",
"MD",
"MC",
"MN",
"ME",
"MS",
"MA",
"MZ",
"MM",
"NA",
"NR",
"NP",
"NL",
"NC",
"NZ",
"NI",
"NE",
"NG",
"NU",
"NF",
"MP",
"NO",
"OM",
"PK",
"PW",
"PS",
"PA",
"PG",
"PY",
"PE",
"PH",
"PN",
"PL",
"PT",
"PR",
"QA",
"RO",
"RU",
"RW",
"RE",
"BL",
"SH",
"KN",
"LC",
"MF",
"PM",
"VC",
"WS",
"SM",
"ST",
"SA",
"SN",
"RS",
"SC",
"SL",
"SG",
"SX",
"SK",
"SI",
"SB",
"SO",
"ZA",
"GS",
"SS",
"ES",
"LK",
"SD",
"SR",
"SJ",
"SZ",
"SE",
"CH",
"SY",
"TW",
"TJ",
"TZ",
"TH",
"TL",
"TG",
"TK",
"TO",
"TT",
"TN",
"TR",
"TM",
"TC",
"TV",
"UG",
"UA",
"AE",
"GB",
"US",
"UM",
"UY",
"UZ",
"VU",
"VE",
"VN",
"VG",
"VI",
"WF",
"EH",
"YE",
"ZM",
"ZW",
]

View File

@@ -0,0 +1,57 @@
"""Helper constants."""
# pylint: disable=missing-class-docstring
from enum import Enum
class HacsCategory(str, Enum):
APPDAEMON = "appdaemon"
INTEGRATION = "integration"
LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script"
THEME = "theme"
REMOVED = "removed"
def __str__(self):
return str(self.value)
class ConfigurationType(str, Enum):
YAML = "yaml"
CONFIG_ENTRY = "config_entry"
class LovelaceMode(str, Enum):
"""Lovelace Modes."""
STORAGE = "storage"
AUTO = "auto"
AUTO_GEN = "auto-gen"
YAML = "yaml"
class HacsStage(str, Enum):
SETUP = "setup"
STARTUP = "startup"
WAITING = "waiting"
RUNNING = "running"
BACKGROUND = "background"
class HacsSetupTask(str, Enum):
WEBSOCKET = "WebSocket API"
FRONTEND = "Frontend"
SENSOR = "Sensor"
HACS_REPO = "Hacs Repository"
CATEGORIES = "Additional categories"
CLEAR_STORAGE = "Clear storage"
class HacsDisabledReason(str, Enum):
RATE_LIMIT = "rate_limit"
REMOVED = "removed"
INVALID_TOKEN = "invalid_token"
CONSTRAINS = "constrains"
LOAD_HACS = "load_hacs"
RESTORE = "restore"

View File

@@ -0,0 +1,21 @@
"""Custom Exceptions for HACS."""
class HacsException(Exception):
"""Super basic."""
class HacsRepositoryArchivedException(HacsException):
"""For repositories that are archived."""
class HacsNotModifiedException(HacsException):
"""For responses that are not modified."""
class HacsExpectedException(HacsException):
"""For stuff that are expected."""
class HacsRepositoryExistException(HacsException):
"""For repositories that are already exist."""

View File

@@ -0,0 +1,208 @@
"""Data handler for HACS."""
import asyncio
import os
from homeassistant.core import callback
from custom_components.hacs.helpers.classes.manifest import HacsManifest
from custom_components.hacs.helpers.functions.register_repository import (
register_repository,
)
from custom_components.hacs.helpers.functions.store import (
async_load_from_store,
async_save_to_store,
async_save_to_store_default_encoder,
get_store_for_key,
)
from custom_components.hacs.share import get_hacs
from custom_components.hacs.utils.logger import getLogger
def update_repository_from_storage(repository, storage_data):
"""Merge in data from storage into the repo data."""
repository.data.memorize_storage(storage_data)
repository.data.update_data(storage_data)
if repository.data.installed:
return
repository.logger.debug("%s Should be installed but is not... Fixing that!", repository)
repository.data.installed = True
class HacsData:
"""HacsData class."""
def __init__(self):
"""Initialize."""
self.logger = getLogger()
self.hacs = get_hacs()
self.content = {}
async def async_write(self):
"""Write content to the store files."""
if self.hacs.status.background_task or self.hacs.system.disabled:
return
self.logger.debug("Saving data")
# Hacs
await async_save_to_store(
self.hacs.hass,
"hacs",
{
"view": self.hacs.configuration.frontend_mode,
"compact": self.hacs.configuration.frontend_compact,
"onboarding_done": self.hacs.configuration.onboarding_done,
"archived_repositories": self.hacs.common.archived_repositories,
"renamed_repositories": self.hacs.common.renamed_repositories,
},
)
await self._async_store_content_and_repos()
for event in ("hacs/repository", "hacs/config"):
self.hacs.hass.bus.async_fire(event, {})
async def _async_store_content_and_repos(self): # bb: ignore
"""Store the main repos file and each repo that is out of date."""
# Repositories
self.content = {}
# Not run concurrently since this is bound by disk I/O
for repository in self.hacs.repositories:
await self.async_store_repository_data(repository)
await async_save_to_store(self.hacs.hass, "repositories", self.content)
async def async_store_repository_data(self, repository):
repository_manifest = repository.repository_manifest.manifest
data = {
"authors": repository.data.authors,
"category": repository.data.category,
"description": repository.data.description,
"domain": repository.data.domain,
"downloads": repository.data.downloads,
"etag_repository": repository.data.etag_repository,
"full_name": repository.data.full_name,
"first_install": repository.status.first_install,
"installed_commit": repository.data.installed_commit,
"installed": repository.data.installed,
"last_commit": repository.data.last_commit,
"last_release_tag": repository.data.last_version,
"last_updated": repository.data.last_updated,
"name": repository.data.name,
"new": repository.data.new,
"repository_manifest": repository_manifest,
"selected_tag": repository.data.selected_tag,
"show_beta": repository.data.show_beta,
"stars": repository.data.stargazers_count,
"topics": repository.data.topics,
"version_installed": repository.data.installed_version,
}
self.content[str(repository.data.id)] = data
if (
repository.data.installed
and (repository.data.installed_commit or repository.data.installed_version)
and (export := repository.data.export_data())
):
# export_data will return `None` if the memorized
# data is already up to date which allows us to avoid
# writing data that is already up to date or generating
# executor jobs to check the data on disk to see
# if a write is needed.
await async_save_to_store_default_encoder(
self.hacs.hass,
f"hacs/{repository.data.id}.hacs",
export,
)
repository.data.memorize_storage(export)
async def restore(self):
"""Restore saved data."""
hacs = await async_load_from_store(self.hacs.hass, "hacs")
repositories = await async_load_from_store(self.hacs.hass, "repositories") or {}
if not hacs and not repositories:
# Assume new install
self.hacs.status.new = True
return True
self.logger.info("Restore started")
self.hacs.status.new = False
# Hacs
self.hacs.configuration.frontend_mode = hacs.get("view", "Grid")
self.hacs.configuration.frontend_compact = hacs.get("compact", False)
self.hacs.configuration.onboarding_done = hacs.get("onboarding_done", False)
self.hacs.common.archived_repositories = hacs.get("archived_repositories", [])
self.hacs.common.renamed_repositories = hacs.get("renamed_repositories", {})
# Repositories
hass = self.hacs.hass
stores = {}
try:
await self.register_unknown_repositories(repositories)
for entry, repo_data in repositories.items():
if self.async_restore_repository(entry, repo_data):
stores[entry] = get_store_for_key(hass, f"hacs/{entry}.hacs")
def _load_from_storage():
for entry, store in stores.items():
if os.path.exists(store.path) and (data := store.load()):
update_repository_from_storage(self.hacs.get_by_id(entry), data)
await hass.async_add_executor_job(_load_from_storage)
self.logger.info("Restore done")
except (Exception, BaseException) as exception: # pylint: disable=broad-except
self.logger.critical(f"[{exception}] Restore Failed!", exc_info=exception)
return False
return True
async def register_unknown_repositories(self, repositories):
"""Registry any unknown repositories."""
register_tasks = [
register_repository(repo_data["full_name"], repo_data["category"], False)
for entry, repo_data in repositories.items()
if not self.hacs.is_known(entry)
]
if register_tasks:
await asyncio.gather(*register_tasks)
@callback
def async_restore_repository(self, entry, repository_data):
full_name = repository_data["full_name"]
if not (repository := self.hacs.get_by_name(full_name)):
self.logger.error(f"Did not find {full_name} ({entry})")
return False
# Restore repository attributes
self.hacs.async_set_repository_id(repository, entry)
repository.data.authors = repository_data.get("authors", [])
repository.data.description = repository_data.get("description")
repository.releases.last_release_object_downloads = repository_data.get("downloads")
repository.data.last_updated = repository_data.get("last_updated")
repository.data.etag_repository = repository_data.get("etag_repository")
repository.data.topics = repository_data.get("topics", [])
repository.data.domain = repository_data.get("domain", None)
repository.data.stargazers_count = repository_data.get("stars", 0)
repository.releases.last_release = repository_data.get("last_release_tag")
repository.data.hide = repository_data.get("hide", False)
repository.data.installed = repository_data.get("installed", False)
repository.data.new = repository_data.get("new", True)
repository.data.selected_tag = repository_data.get("selected_tag")
repository.data.show_beta = repository_data.get("show_beta", False)
repository.data.last_version = repository_data.get("last_release_tag")
repository.data.last_commit = repository_data.get("last_commit")
repository.data.installed_version = repository_data.get("version_installed")
repository.data.installed_commit = repository_data.get("installed_commit")
repository.repository_manifest = HacsManifest.from_dict(
repository_data.get("repository_manifest", {})
)
if repository.data.installed:
repository.status.first_install = False
if repository_data["full_name"] == "hacs/integration":
repository.data.installed_version = self.hacs.version
repository.data.installed = True
return True

View File

@@ -0,0 +1,338 @@
"""Initialize the HACS base."""
from datetime import timedelta
from aiogithubapi import GitHubException
from aiogithubapi.exceptions import GitHubNotModifiedException
from queueman import QueueManager
from queueman.exceptions import QueueManagerExecutionStillInProgress
from custom_components.hacs.helpers import HacsHelpers
from custom_components.hacs.helpers.functions.get_list_from_default import (
async_get_list_from_default,
)
from custom_components.hacs.helpers.functions.register_repository import (
register_repository,
)
from custom_components.hacs.helpers.functions.store import (
async_load_from_store,
async_save_to_store,
)
from custom_components.hacs.share import (
get_removed,
is_removed,
list_removed_repositories,
)
from ..base import HacsBase
from ..enums import HacsCategory, HacsStage
from ..share import get_factory, get_queue
class Hacs(HacsBase, HacsHelpers):
"""The base class of HACS, nested throughout the project."""
factory = get_factory()
queue = get_queue()
@property
def repositories(self):
"""Return the full repositories list."""
return self._repositories
def async_set_repositories(self, repositories):
"""Set the list of repositories."""
self._repositories = []
self._repositories_by_id = {}
self._repositories_by_full_name = {}
for repository in repositories:
self.async_add_repository(repository)
def async_set_repository_id(self, repository, repo_id):
"""Update a repository id."""
existing_repo_id = str(repository.data.id)
if existing_repo_id == repo_id:
return
if existing_repo_id != "0":
raise ValueError(
f"The repo id for {repository.data.full_name_lower} is already set to {existing_repo_id}"
)
repository.data.id = repo_id
self._repositories_by_id[repo_id] = repository
def async_add_repository(self, repository):
"""Add a repository to the list."""
if repository.data.full_name_lower in self._repositories_by_full_name:
raise ValueError(f"The repo {repository.data.full_name_lower} is already added")
self._repositories.append(repository)
repo_id = str(repository.data.id)
if repo_id != "0":
self._repositories_by_id[repo_id] = repository
self._repositories_by_full_name[repository.data.full_name_lower] = repository
def async_remove_repository(self, repository):
"""Remove a repository from the list."""
if repository.data.full_name_lower not in self._repositories_by_full_name:
return
self._repositories.remove(repository)
repo_id = str(repository.data.id)
if repo_id in self._repositories_by_id:
del self._repositories_by_id[repo_id]
del self._repositories_by_full_name[repository.data.full_name_lower]
def get_by_id(self, repository_id):
"""Get repository by ID."""
return self._repositories_by_id.get(str(repository_id))
def get_by_name(self, repository_full_name):
"""Get repository by full_name."""
if repository_full_name is None:
return None
return self._repositories_by_full_name.get(repository_full_name.lower())
def is_known(self, repository_id):
"""Return a bool if the repository is known."""
return str(repository_id) in self._repositories_by_id
@property
def sorted_by_name(self):
"""Return a sorted(by name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.display_name)
@property
def sorted_by_repository_name(self):
"""Return a sorted(by repository_name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.data.full_name)
async def register_repository(self, full_name, category, check=True):
"""Register a repository."""
await register_repository(full_name, category, check=check)
async def startup_tasks(self, _event=None):
"""Tasks that are started after startup."""
await self.async_set_stage(HacsStage.STARTUP)
self.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
await self.handle_critical_repositories_startup()
await self.async_load_default_repositories()
await self.clear_out_removed_repositories()
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.recurring_tasks_installed, timedelta(hours=2)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.recurring_tasks_all, timedelta(hours=25)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.prosess_queue, timedelta(minutes=10)
)
)
self.hass.bus.async_fire("hacs/reload", {"force": True})
await self.recurring_tasks_installed()
await self.prosess_queue()
self.status.startup = False
self.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.async_set_stage(HacsStage.RUNNING)
async def handle_critical_repositories_startup(self):
"""Handled critical repositories during startup."""
alert = False
critical = await async_load_from_store(self.hass, "critical")
if not critical:
return
for repo in critical:
if not repo["acknowledged"]:
alert = True
if alert:
self.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
)
async def handle_critical_repositories(self):
"""Handled critical repositories during runtime."""
# Get critical repositories
critical_queue = QueueManager()
instored = []
critical = []
was_installed = False
try:
critical = await self.async_github_get_hacs_default_file("critical")
except GitHubNotModifiedException:
return
except GitHubException:
pass
if not critical:
self.log.debug("No critical repositories")
return
stored_critical = await async_load_from_store(self.hass, "critical")
for stored in stored_critical or []:
instored.append(stored["repository"])
stored_critical = []
for repository in critical:
removed_repo = get_removed(repository["repository"])
removed_repo.removal_type = "critical"
repo = self.get_by_name(repository["repository"])
stored = {
"repository": repository["repository"],
"reason": repository["reason"],
"link": repository["link"],
"acknowledged": True,
}
if repository["repository"] not in instored:
if repo is not None and repo.installed:
self.log.critical(
"Removing repository %s, it is marked as critical",
repository["repository"],
)
was_installed = True
stored["acknowledged"] = False
# Remove from HACS
critical_queue.add(repository.uninstall())
repo.remove()
stored_critical.append(stored)
removed_repo.update_data(stored)
# Uninstall
await critical_queue.execute()
# Save to FS
await async_save_to_store(self.hass, "critical", stored_critical)
# Restart HASS
if was_installed:
self.log.critical("Resarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
async def prosess_queue(self, _notarealarg=None):
"""Recurring tasks for installed repositories."""
if not self.queue.has_pending_tasks:
self.log.debug("Nothing in the queue")
return
if self.queue.running:
self.log.debug("Queue is already running")
return
can_update = await self.async_can_update()
self.log.debug(
"Can update %s repositories, items in queue %s",
can_update,
self.queue.pending_tasks,
)
if can_update != 0:
self.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
try:
await self.queue.execute(can_update)
except QueueManagerExecutionStillInProgress:
pass
self.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
async def recurring_tasks_installed(self, _notarealarg=None):
"""Recurring tasks for installed repositories."""
self.log.debug("Starting recurring background task for installed repositories")
self.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
for repository in self.repositories:
if self.status.startup and repository.data.full_name == "hacs/integration":
continue
if repository.data.installed and repository.data.category in self.common.categories:
self.queue.add(self.factory.safe_update(repository))
await self.handle_critical_repositories()
self.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.log.debug("Recurring background task for installed repositories done")
async def recurring_tasks_all(self, _notarealarg=None):
"""Recurring tasks for all repositories."""
self.log.debug("Starting recurring background task for all repositories")
self.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
for repository in self.repositories:
if repository.data.category in self.common.categories:
self.queue.add(self.factory.safe_common_update(repository))
await self.async_load_default_repositories()
await self.clear_out_removed_repositories()
self.status.background_task = False
await self.data.async_write()
self.hass.bus.async_fire("hacs/status", {})
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
self.log.debug("Recurring background task for all repositories done")
async def clear_out_removed_repositories(self):
"""Clear out blaclisted repositories."""
need_to_save = False
for removed in list_removed_repositories():
repository = self.get_by_name(removed.repository)
if repository is not None:
if repository.data.installed and removed.removal_type != "critical":
self.log.warning(
f"You have {repository.data.full_name} installed with HACS "
+ "this repository has been removed, please consider removing it. "
+ f"Removal reason ({removed.removal_type})"
)
else:
need_to_save = True
repository.remove()
if need_to_save:
await self.data.async_write()
async def async_load_default_repositories(self):
"""Load known repositories."""
self.log.info("Loading known repositories")
for item in await async_get_list_from_default(HacsCategory.REMOVED):
removed = get_removed(item["repository"])
removed.reason = item.get("reason")
removed.link = item.get("link")
removed.removal_type = item.get("removal_type")
for category in self.common.categories or []:
self.queue.add(self.async_get_category_repositories(HacsCategory(category)))
await self.prosess_queue()
async def async_get_category_repositories(self, category: HacsCategory):
"""Get repositories from category."""
repositories = await async_get_list_from_default(category)
for repo in repositories:
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if is_removed(repo):
continue
if repo in self.common.archived_repositories:
continue
repository = self.get_by_name(repo)
if repository is not None:
if str(repository.data.id) not in self.common.default:
self.common.default.append(str(repository.data.id))
else:
continue
continue
self.queue.add(self.factory.safe_register(repo, category))

View File

@@ -0,0 +1,17 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from custom_components.hacs.helpers.methods import (
HacsHelperMethods,
RepositoryHelperMethods,
)
from custom_components.hacs.helpers.properties import RepositoryHelperProperties
class RepositoryHelpers(
RepositoryHelperMethods,
RepositoryHelperProperties,
):
"""Helper class for repositories"""
class HacsHelpers(HacsHelperMethods):
"""Helper class for HACS"""

View File

@@ -0,0 +1,47 @@
"""
Manifest handling of a repository.
https://hacs.xyz/docs/publish/start#hacsjson
"""
from typing import List
import attr
from custom_components.hacs.exceptions import HacsException
@attr.s(auto_attribs=True)
class HacsManifest:
"""HacsManifest class."""
name: str = None
content_in_root: bool = False
zip_release: bool = False
filename: str = None
manifest: dict = {}
hacs: str = None
hide_default_branch: bool = False
domains: List[str] = []
country: List[str] = []
homeassistant: str = None
persistent_directory: str = None
iot_class: str = None
render_readme: bool = False
@staticmethod
def from_dict(manifest: dict):
"""Set attributes from dicts."""
if manifest is None:
raise HacsException("Missing manifest data")
manifest_data = HacsManifest()
manifest_data.manifest = manifest
if country := manifest.get("country"):
if isinstance(country, str):
manifest["country"] = [country]
for key in manifest:
setattr(manifest_data, key, manifest[key])
return manifest_data

View File

@@ -0,0 +1,21 @@
"""Object for removed repositories."""
import attr
@attr.s(auto_attribs=True)
class RemovedRepository:
repository: str = None
reason: str = None
link: str = None
removal_type: str = None # archived, not_compliant, critical, dev, broken
acknowledged: bool = False
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if key in self.__dict__:
setattr(self, key, data[key])
def to_json(self):
"""Return a JSON representation of the data."""
return attr.asdict(self)

View File

@@ -0,0 +1,459 @@
"""Repository."""
# pylint: disable=broad-except, no-member
import json
import os
import shutil
import tempfile
import zipfile
from aiogithubapi import AIOGitHubAPIException
from queueman import QueueManager
from custom_components.hacs.exceptions import HacsException, HacsNotModifiedException
from custom_components.hacs.helpers import RepositoryHelpers
from custom_components.hacs.helpers.classes.manifest import HacsManifest
from custom_components.hacs.helpers.classes.repositorydata import RepositoryData
from custom_components.hacs.helpers.classes.validate import Validate
from custom_components.hacs.helpers.functions.download import async_download_file
from custom_components.hacs.helpers.functions.information import (
get_info_md_content,
get_repository,
)
from custom_components.hacs.helpers.functions.is_safe_to_remove import is_safe_to_remove
from custom_components.hacs.helpers.functions.misc import get_repository_name
from custom_components.hacs.helpers.functions.save import async_save_file
from custom_components.hacs.helpers.functions.store import async_remove_store
from custom_components.hacs.helpers.functions.validate_repository import (
common_update_data,
common_validate,
)
from custom_components.hacs.helpers.functions.version_to_install import (
version_to_install,
)
from custom_components.hacs.share import get_hacs
from custom_components.hacs.utils.logger import getLogger
class RepositoryVersions:
"""Versions."""
available = None
available_commit = None
installed = None
installed_commit = None
class RepositoryStatus:
"""Repository status."""
hide = False
installed = False
last_updated = None
new = True
selected_tag = None
show_beta = False
track = True
updated_info = False
first_install = True
class RepositoryInformation:
"""RepositoryInformation."""
additional_info = None
authors = []
category = None
default_branch = None
description = ""
state = None
full_name = None
full_name_lower = None
file_name = None
javascript_type = None
homeassistant_version = None
last_updated = None
uid = None
stars = 0
info = None
name = None
topics = []
class RepositoryReleases:
"""RepositoyReleases."""
last_release = None
last_release_object = None
last_release_object_downloads = None
published_tags = []
objects = []
releases = False
downloads = None
class RepositoryPath:
"""RepositoryPath."""
local = None
remote = None
class RepositoryContent:
"""RepositoryContent."""
path = None
files = []
objects = []
single = False
class HacsRepository(RepositoryHelpers):
"""HacsRepository."""
def __init__(self):
"""Set up HacsRepository."""
self.hacs = get_hacs()
self.data = RepositoryData()
self.content = RepositoryContent()
self.content.path = RepositoryPath()
self.information = RepositoryInformation()
self.repository_object = None
self.status = RepositoryStatus()
self.state = None
self.force_branch = False
self.integration_manifest = {}
self.repository_manifest = HacsManifest.from_dict({})
self.validate = Validate()
self.releases = RepositoryReleases()
self.versions = RepositoryVersions()
self.pending_restart = False
self.tree = []
self.treefiles = []
self.ref = None
self.logger = getLogger()
def __str__(self) -> str:
"""Return a string representation of the repository."""
return f"<{self.data.category.title()} {self.data.full_name}>"
@property
def display_name(self):
"""Return display name."""
return get_repository_name(self)
@property
def ignored_by_country_configuration(self) -> bool:
"""Return True if hidden by country."""
if self.data.installed:
return False
configuration = self.hacs.configuration.country.lower()
manifest = [entry.lower() for entry in self.repository_manifest.country or []]
if configuration == "all":
return False
if not manifest:
return False
return configuration not in manifest
@property
def display_status(self):
"""Return display_status."""
if self.data.new:
status = "new"
elif self.pending_restart:
status = "pending-restart"
elif self.pending_upgrade:
status = "pending-upgrade"
elif self.data.installed:
status = "installed"
else:
status = "default"
return status
@property
def display_status_description(self):
"""Return display_status_description."""
description = {
"default": "Not installed.",
"pending-restart": "Restart pending.",
"pending-upgrade": "Upgrade pending.",
"installed": "No action required.",
"new": "This is a newly added repository.",
}
return description[self.display_status]
@property
def display_installed_version(self):
"""Return display_authors"""
if self.data.installed_version is not None:
installed = self.data.installed_version
else:
if self.data.installed_commit is not None:
installed = self.data.installed_commit
else:
installed = ""
return installed
@property
def display_available_version(self):
"""Return display_authors"""
if self.data.last_version is not None:
available = self.data.last_version
else:
if self.data.last_commit is not None:
available = self.data.last_commit
else:
available = ""
return available
@property
def display_version_or_commit(self):
"""Does the repositoriy use releases or commits?"""
if self.data.releases:
version_or_commit = "version"
else:
version_or_commit = "commit"
return version_or_commit
@property
def main_action(self):
"""Return the main action."""
actions = {
"new": "INSTALL",
"default": "INSTALL",
"installed": "REINSTALL",
"pending-restart": "REINSTALL",
"pending-upgrade": "UPGRADE",
}
return actions[self.display_status]
async def common_validate(self, ignore_issues=False):
"""Common validation steps of the repository."""
await common_validate(self, ignore_issues)
async def common_registration(self):
"""Common registration steps of the repository."""
# Attach repository
if self.repository_object is None:
try:
self.repository_object, etag = await get_repository(
self.hacs.session,
self.hacs.configuration.token,
self.data.full_name,
None if self.data.installed else self.data.etag_repository,
)
self.data.update_data(self.repository_object.attributes)
self.data.etag_repository = etag
except HacsNotModifiedException:
self.logger.debug(
"Did not update %s, content was not modified", self.data.full_name
)
return
# Set topics
self.data.topics = self.data.topics
# Set stargazers_count
self.data.stargazers_count = self.data.stargazers_count
# Set description
self.data.description = self.data.description
if self.hacs.system.action:
if self.data.description is None or len(self.data.description) == 0:
raise HacsException("::error:: Missing repository description")
async def common_update(self, ignore_issues=False, force=False):
"""Common information update steps of the repository."""
self.logger.debug("%s Getting repository information", self)
# Attach repository
current_etag = self.data.etag_repository
await common_update_data(self, ignore_issues, force)
if not self.data.installed and (current_etag == self.data.etag_repository) and not force:
self.logger.debug("Did not update %s, content was not modified", self.data.full_name)
return False
# Update last updated
self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0)
# Update last available commit
await self.repository_object.set_last_commit()
self.data.last_commit = self.repository_object.last_commit
# Get the content of hacs.json
await self.get_repository_manifest_content()
# Update "info.md"
self.information.additional_info = await get_info_md_content(self)
return True
async def download_zip_files(self, validate):
"""Download ZIP archive from repository release."""
download_queue = QueueManager()
try:
contents = False
for release in self.releases.objects:
self.logger.info("%s ref: %s --- tag: %s.", self, self.ref, release.tag_name)
if release.tag_name == self.ref.split("/")[1]:
contents = release.assets
if not contents:
return validate
for content in contents or []:
download_queue.add(self.async_download_zip_file(content, validate))
await download_queue.execute()
except (Exception, BaseException):
validate.errors.append("Download was not completed")
return validate
async def async_download_zip_file(self, content, validate):
"""Download ZIP archive from repository release."""
try:
filecontent = await async_download_file(content.download_url)
if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded")
return
temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp)
temp_file = f"{temp_dir}/{self.data.filename}"
result = await async_save_file(temp_file, filecontent)
with zipfile.ZipFile(temp_file, "r") as zip_file:
zip_file.extractall(self.content.path.local)
def cleanup_temp_dir():
"""Cleanup temp_dir."""
if os.path.exists(temp_dir):
self.logger.debug("Cleaning up %s", temp_dir)
shutil.rmtree(temp_dir)
if result:
self.logger.info("%s Download of %s completed", self, content.name)
await self.hacs.hass.async_add_executor_job(cleanup_temp_dir)
return
validate.errors.append(f"[{content.name}] was not downloaded")
except (Exception, BaseException):
validate.errors.append("Download was not completed")
return validate
async def download_content(self, validate, _directory_path, _local_directory, _ref):
"""Download the content of a directory."""
from custom_components.hacs.helpers.functions.download import download_content
validate = await download_content(self)
return validate
async def get_repository_manifest_content(self):
"""Get the content of the hacs.json file."""
if not "hacs.json" in [x.filename for x in self.tree]:
if self.hacs.system.action:
raise HacsException("::error:: No hacs.json file in the root of the repository.")
return
if self.hacs.system.action:
self.logger.info("%s Found hacs.json", self)
self.ref = version_to_install(self)
try:
manifest = await self.repository_object.get_contents("hacs.json", self.ref)
self.repository_manifest = HacsManifest.from_dict(json.loads(manifest.content))
self.data.update_data(json.loads(manifest.content))
except (AIOGitHubAPIException, Exception) as exception: # Gotta Catch 'Em All
if self.hacs.system.action:
raise HacsException(
f"::error:: hacs.json file is not valid ({exception})."
) from None
if self.hacs.system.action:
self.logger.info("%s hacs.json is valid", self)
def remove(self):
"""Run remove tasks."""
self.logger.info("%s Starting removal", self)
if self.data.id in self.hacs.common.installed:
self.hacs.common.installed.remove(self.data.id)
for repository in self.hacs.repositories:
if repository.data.id == self.data.id:
self.hacs.async_remove_repository(repository)
async def uninstall(self):
"""Run uninstall tasks."""
self.logger.info("%s Uninstalling", self)
if not await self.remove_local_directory():
raise HacsException("Could not uninstall")
self.data.installed = False
if self.data.category == "integration":
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 (Exception, BaseException): # pylint: disable=broad-except
pass
if self.data.full_name in self.hacs.common.installed:
self.hacs.common.installed.remove(self.data.full_name)
await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs")
self.data.installed_version = None
self.data.installed_commit = None
self.hacs.hass.bus.async_fire(
"hacs/repository",
{"id": 1337, "action": "uninstall", "repository": self.data.full_name},
)
async def remove_local_directory(self):
"""Check the local directory."""
from asyncio import sleep
try:
if self.data.category == "python_script":
local_path = f"{self.content.path.local}/{self.data.name}.py"
elif self.data.category == "theme":
if os.path.exists(
f"{self.hacs.core.config_path}/{self.hacs.configuration.theme_path}/{self.data.name}.yaml"
):
os.remove(
f"{self.hacs.core.config_path}/{self.hacs.configuration.theme_path}/{self.data.name}.yaml"
)
local_path = self.content.path.local
elif self.data.category == "integration":
if not self.data.domain:
self.logger.error("%s Missing domain", self)
return False
local_path = self.content.path.local
else:
local_path = self.content.path.local
if os.path.exists(local_path):
if not is_safe_to_remove(local_path):
self.logger.error("%s Path %s is blocked from removal", self, local_path)
return False
self.logger.debug("%s Removing %s", self, local_path)
if self.data.category in ["python_script"]:
os.remove(local_path)
else:
shutil.rmtree(local_path)
while os.path.exists(local_path):
await sleep(1)
else:
self.logger.debug(
"%s Presumed local content path %s does not exist", self, local_path
)
except (Exception, BaseException) as exception:
self.logger.debug("%s Removing %s failed with %s", self, local_path, exception)
return False
return True

View File

@@ -0,0 +1,146 @@
"""Repository data."""
from datetime import datetime
import json
from typing import List, Optional
import attr
from homeassistant.helpers.json import JSONEncoder
@attr.s(auto_attribs=True)
class RepositoryData:
"""RepositoryData class."""
archived: bool = False
authors: List[str] = []
category: str = ""
content_in_root: bool = False
country: List[str] = []
config_flow: bool = False
default_branch: str = None
description: str = ""
domain: str = ""
domains: List[str] = []
downloads: int = 0
etag_repository: str = None
file_name: str = ""
filename: str = ""
first_install: bool = False
fork: bool = False
full_name: str = ""
hacs: str = None # Minimum HACS version
hide: bool = False
hide_default_branch: bool = False
homeassistant: str = None # Minimum Home Assistant version
id: int = 0
iot_class: str = None
installed: bool = False
installed_commit: str = None
installed_version: str = None
open_issues: int = 0
last_commit: str = None
last_version: str = None
last_updated: str = 0
manifest_name: str = None
new: bool = True
persistent_directory: str = None
pushed_at: str = ""
releases: bool = False
render_readme: bool = False
published_tags: List[str] = []
selected_tag: str = None
show_beta: bool = False
stargazers_count: int = 0
topics: List[str] = []
zip_release: bool = False
_storage_data: Optional[dict] = None
@property
def stars(self):
"""Return the stargazers count."""
return self.stargazers_count or 0
@property
def name(self):
"""Return the name."""
if self.category in ["integration", "netdaemon"]:
return self.domain
return self.full_name.split("/")[-1]
def to_json(self):
"""Export to json."""
return attr.asdict(self, filter=lambda attr, _: attr.name != "_storage_data")
def memorize_storage(self, data) -> None:
"""Memorize the storage data."""
self._storage_data = data
def export_data(self) -> Optional[dict]:
"""Export to json if the data has changed.
Returns the data to export if the data needs
to be written.
Returns None if the data has not changed.
"""
export = json.loads(json.dumps(self.to_json(), cls=JSONEncoder))
return None if self._storage_data == export else export
@staticmethod
def create_from_dict(source: dict):
"""Set attributes from dicts."""
data = RepositoryData()
for key in source:
if key not in data.__dict__:
continue
if key == "pushed_at":
if source[key] == "":
continue
if "Z" in source[key]:
setattr(
data,
key,
datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ"),
)
else:
setattr(
data,
key,
datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%S"),
)
elif key == "id":
setattr(data, key, str(source[key]))
elif key == "country":
if isinstance(source[key], str):
setattr(data, key, [source[key]])
else:
setattr(data, key, source[key])
else:
setattr(data, key, source[key])
return data
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if key not in self.__dict__:
continue
if key == "pushed_at":
if data[key] == "":
continue
if "Z" in data[key]:
setattr(
self,
key,
datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ"),
)
else:
setattr(self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%S"))
elif key == "id":
setattr(self, key, str(data[key]))
elif key == "country":
if isinstance(data[key], str):
setattr(self, key, [data[key]])
else:
setattr(self, key, data[key])
else:
setattr(self, key, data[key])

View File

@@ -0,0 +1,11 @@
class Validate:
"""Validate."""
errors = []
@property
def success(self):
"""Return bool if the validation was a success."""
if self.errors:
return False
return True

View File

@@ -0,0 +1,74 @@
"""HACS Configuration Schemas."""
# pylint: disable=dangerous-default-value
import voluptuous as vol
from custom_components.hacs.const import LOCALE
# Configuration:
TOKEN = "token"
SIDEPANEL_TITLE = "sidepanel_title"
SIDEPANEL_ICON = "sidepanel_icon"
FRONTEND_REPO = "frontend_repo"
FRONTEND_REPO_URL = "frontend_repo_url"
APPDAEMON = "appdaemon"
NETDAEMON = "netdaemon"
# Options:
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

@@ -0,0 +1,234 @@
"""Helpers to download repository content."""
import os
import pathlib
import tempfile
import zipfile
import async_timeout
import backoff
from queueman import QueueManager, concurrent
from custom_components.hacs.exceptions import HacsException
from custom_components.hacs.helpers.functions.filters import (
filter_content_return_one_of_type,
)
from custom_components.hacs.helpers.functions.save import async_save_file
from custom_components.hacs.share import get_hacs
from custom_components.hacs.utils.logger import getLogger
_LOGGER = getLogger()
class FileInformation:
def __init__(self, url, path, name):
self.download_url = url
self.path = path
self.name = name
@backoff.on_exception(backoff.expo, Exception, max_tries=5)
async def async_download_file(url):
"""Download files, and return the content."""
hacs = get_hacs()
if url is None:
return
if "tags/" in url:
url = url.replace("tags/", "")
_LOGGER.debug("Downloading %s", url)
result = None
with async_timeout.timeout(60, loop=hacs.hass.loop):
request = await hacs.session.get(url)
# Make sure that we got a valid result
if request.status == 200:
result = await request.read()
else:
raise HacsException(f"Got status code {request.status} when trying to download {url}")
return result
def should_try_releases(repository):
"""Return a boolean indicating whether to download releases or not."""
if repository.data.zip_release:
if repository.data.filename.endswith(".zip"):
if repository.ref != repository.data.default_branch:
return True
if repository.ref == repository.data.default_branch:
return False
if repository.data.category not in ["plugin", "theme"]:
return False
if not repository.data.releases:
return False
return True
def gather_files_to_download(repository):
"""Return a list of file objects to be downloaded."""
files = []
tree = repository.tree
ref = f"{repository.ref}".replace("tags/", "")
releaseobjects = repository.releases.objects
category = repository.data.category
remotelocation = repository.content.path.remote
if should_try_releases(repository):
for release in releaseobjects or []:
if ref == release.tag_name:
for asset in release.assets or []:
files.append(asset)
if files:
return files
if repository.content.single:
for treefile in tree:
if treefile.filename == repository.data.file_name:
files.append(
FileInformation(treefile.download_url, treefile.full_path, treefile.filename)
)
return files
if category == "plugin":
for treefile in tree:
if treefile.path in ["", "dist"]:
if remotelocation == "dist" and not treefile.filename.startswith("dist"):
continue
if not remotelocation:
if not treefile.filename.endswith(".js"):
continue
if treefile.path != "":
continue
if not treefile.is_directory:
files.append(
FileInformation(
treefile.download_url, treefile.full_path, treefile.filename
)
)
if files:
return files
if repository.data.content_in_root:
if not repository.data.filename:
if category == "theme":
tree = filter_content_return_one_of_type(repository.tree, "", "yaml", "full_path")
for path in tree:
if path.is_directory:
continue
if path.full_path.startswith(repository.content.path.remote):
files.append(FileInformation(path.download_url, path.full_path, path.filename))
return files
async def download_zip_files(repository, validate):
"""Download ZIP archive from repository release."""
contents = []
queue = QueueManager()
try:
for release in repository.releases.objects:
repository.logger.info(f"ref: {repository.ref} --- tag: {release.tag_name}")
if release.tag_name == repository.ref.split("/")[1]:
contents = release.assets
if not contents:
return validate
for content in contents or []:
queue.add(async_download_zip_file(repository, content, validate))
await queue.execute()
except (Exception, BaseException) as exception: # pylint: disable=broad-except
validate.errors.append(f"Download was not completed [{exception}]")
return validate
async def async_download_zip_file(repository, content, validate):
"""Download ZIP archive from repository release."""
try:
filecontent = await async_download_file(content.download_url)
if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded.")
return
result = await async_save_file(
f"{tempfile.gettempdir()}/{repository.data.filename}", filecontent
)
with zipfile.ZipFile(
f"{tempfile.gettempdir()}/{repository.data.filename}", "r"
) as zip_file:
zip_file.extractall(repository.content.path.local)
os.remove(f"{tempfile.gettempdir()}/{repository.data.filename}")
if result:
repository.logger.info(f"Download of {content.name} completed")
return
validate.errors.append(f"[{content.name}] was not downloaded.")
except (Exception, BaseException) as exception: # pylint: disable=broad-except
validate.errors.append(f"Download was not completed [{exception}]")
return validate
async def download_content(repository):
"""Download the content of a directory."""
queue = QueueManager()
contents = gather_files_to_download(repository)
repository.logger.debug(repository.data.filename)
if not contents:
raise HacsException("No content to download")
for content in contents:
if repository.data.content_in_root and repository.data.filename:
if content.name != repository.data.filename:
continue
queue.add(dowload_repository_content(repository, content))
await queue.execute()
return repository.validate
@concurrent(10)
async def dowload_repository_content(repository, content):
"""Download content."""
try:
repository.logger.debug(f"Downloading {content.name}")
filecontent = await async_download_file(content.download_url)
if filecontent is None:
repository.validate.errors.append(f"[{content.name}] was not downloaded.")
return
# Save the content of the file.
if repository.content.single or content.path is None:
local_directory = repository.content.path.local
else:
_content_path = content.path
if not repository.data.content_in_root:
_content_path = _content_path.replace(f"{repository.content.path.remote}", "")
local_directory = f"{repository.content.path.local}/{_content_path}"
local_directory = local_directory.split("/")
del local_directory[-1]
local_directory = "/".join(local_directory)
# Check local directory
pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True)
local_file_path = (f"{local_directory}/{content.name}").replace("//", "/")
result = await async_save_file(local_file_path, filecontent)
if result:
repository.logger.info(f"Download of {content.name} completed")
return
repository.validate.errors.append(f"[{content.name}] was not downloaded.")
except (Exception, BaseException) as exception: # pylint: disable=broad-except
repository.validate.errors.append(f"Download was not completed [{exception}]")

View File

@@ -0,0 +1,53 @@
"""Filter functions."""
def filter_content_return_one_of_type(content, namestartswith, filterfiltype, attr="name"):
"""Only match 1 of the filter."""
contents = []
filetypefound = False
for filename in content:
if isinstance(filename, str):
if filename.startswith(namestartswith):
if filename.endswith(f".{filterfiltype}"):
if not filetypefound:
contents.append(filename)
filetypefound = True
continue
else:
contents.append(filename)
else:
if getattr(filename, attr).startswith(namestartswith):
if getattr(filename, attr).endswith(f".{filterfiltype}"):
if not filetypefound:
contents.append(filename)
filetypefound = True
continue
else:
contents.append(filename)
return contents
def find_first_of_filetype(content, filterfiltype, attr="name"):
"""Find the first of the file type."""
filename = ""
for _filename in content:
if isinstance(_filename, str):
if _filename.endswith(f".{filterfiltype}"):
filename = _filename
break
else:
if getattr(_filename, attr).endswith(f".{filterfiltype}"):
filename = getattr(_filename, attr)
break
return filename
def get_first_directory_in_directory(content, dirname):
"""Return the first directory in dirname or None."""
directory = None
for path in content:
if path.full_path.startswith(dirname) and path.full_path != dirname:
if path.is_directory:
directory = path.filename
break
return directory

View File

@@ -0,0 +1,37 @@
"""Helper to get default repositories."""
from typing import List
from aiogithubapi import (
GitHubAuthenticationException,
GitHubNotModifiedException,
GitHubRatelimitException,
)
from custom_components.hacs.const import REPOSITORY_HACS_DEFAULT
from custom_components.hacs.enums import HacsCategory, HacsDisabledReason
from custom_components.hacs.share import get_hacs
async def async_get_list_from_default(default: HacsCategory) -> List:
"""Get repositories from default list."""
hacs = get_hacs()
repositories = []
try:
repositories = await hacs.async_github_get_hacs_default_file(default)
hacs.log.debug("Got %s elements for %s", len(repositories), default)
except GitHubNotModifiedException:
hacs.log.debug("Content did not change for %s/%s", REPOSITORY_HACS_DEFAULT, default)
except GitHubRatelimitException as exception:
hacs.log.error(exception)
hacs.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except GitHubAuthenticationException as exception:
hacs.log.error(exception)
hacs.disable_hacs(HacsDisabledReason.INVALID_TOKEN)
except BaseException as exception: # pylint: disable=broad-except
hacs.log.error(exception)
return repositories

View File

@@ -0,0 +1,230 @@
"""Return repository information if any."""
import json
from aiogithubapi import AIOGitHubAPIException, AIOGitHubAPINotModifiedException, GitHub
from aiogithubapi.const import ACCEPT_HEADERS
from custom_components.hacs.exceptions import HacsException, HacsNotModifiedException
from custom_components.hacs.helpers.functions.template import render_template
from custom_components.hacs.share import get_hacs
def info_file(repository):
"""get info filename."""
if repository.data.render_readme:
for filename in ["readme", "readme.md", "README", "README.md", "README.MD"]:
if filename in repository.treefiles:
return filename
return ""
for filename in ["info", "info.md", "INFO", "INFO.md", "INFO.MD"]:
if filename in repository.treefiles:
return filename
return ""
async def get_info_md_content(repository):
"""Get the content of info.md"""
filename = info_file(repository)
if not filename:
return ""
try:
info = await repository.repository_object.get_contents(filename, repository.ref)
if info is None:
return ""
info = info.content.replace("<svg", "<disabled").replace("</svg", "</disabled")
return render_template(info, repository)
except (
ValueError,
AIOGitHubAPIException,
Exception, # pylint: disable=broad-except
):
if repository.hacs.system.action:
raise HacsException("::error:: No info file found")
return ""
async def get_repository(session, token, repository_full_name, etag=None):
"""Return a repository object or None."""
hacs = get_hacs()
try:
github = GitHub(
token,
session,
headers={
"User-Agent": f"HACS/{hacs.version}",
"Accept": ACCEPT_HEADERS["preview"],
},
)
repository = await github.get_repo(repository_full_name, etag)
return repository, github.client.last_response.etag
except AIOGitHubAPINotModifiedException as exception:
raise HacsNotModifiedException(exception) from exception
except (ValueError, AIOGitHubAPIException, Exception) as exception:
raise HacsException(exception) from exception
async def get_tree(repository, ref):
"""Return the repository tree."""
try:
tree = await repository.get_tree(ref)
return tree
except (ValueError, AIOGitHubAPIException) as exception:
raise HacsException(exception)
async def get_releases(repository, prerelease=False, returnlimit=5):
"""Return the repository releases."""
try:
releases = await repository.get_releases(prerelease, returnlimit)
return releases
except (ValueError, AIOGitHubAPIException) as exception:
raise HacsException(exception)
def get_frontend_version():
"""get the frontend version from the manifest."""
manifest = read_hacs_manifest()
frontend = 0
for requirement in manifest.get("requirements", []):
if requirement.startswith("hacs_frontend"):
frontend = requirement.split("==")[1]
break
return frontend
def read_hacs_manifest():
"""Reads the HACS manifest file and returns the contents."""
hacs = get_hacs()
content = {}
with open(f"{hacs.core.config_path}/custom_components/hacs/manifest.json") as manifest:
content = json.loads(manifest.read())
return content
async def get_integration_manifest(repository):
"""Return the integration manifest."""
if repository.data.content_in_root:
manifest_path = "manifest.json"
else:
manifest_path = f"{repository.content.path.remote}/manifest.json"
if not manifest_path in [x.full_path for x in repository.tree]:
raise HacsException(f"No file found '{manifest_path}'")
try:
manifest = await repository.repository_object.get_contents(manifest_path, repository.ref)
manifest = json.loads(manifest.content)
except (Exception, BaseException) as exception: # pylint: disable=broad-except
raise HacsException(f"Could not read manifest.json [{exception}]")
try:
repository.integration_manifest = manifest
repository.data.authors = manifest["codeowners"]
repository.data.domain = manifest["domain"]
repository.data.manifest_name = manifest["name"]
repository.data.config_flow = manifest.get("config_flow", False)
if repository.hacs.system.action:
if manifest.get("documentation") is None:
raise HacsException("::error:: manifest.json is missing documentation")
if manifest.get("homeassistant") is not None:
raise HacsException(
"::error:: The homeassistant key in manifest.json is no longer valid"
)
# if manifest.get("issue_tracker") is None:
# raise HacsException("The 'issue_tracker' is missing in manifest.json")
# Set local path
repository.content.path.local = repository.localpath
except KeyError as exception:
raise HacsException(f"Missing expected key {exception} in '{manifest_path}'")
def find_file_name(repository):
"""Get the filename to target."""
if repository.data.category == "plugin":
get_file_name_plugin(repository)
elif repository.data.category == "integration":
get_file_name_integration(repository)
elif repository.data.category == "theme":
get_file_name_theme(repository)
elif repository.data.category == "appdaemon":
get_file_name_appdaemon(repository)
elif repository.data.category == "python_script":
get_file_name_python_script(repository)
if repository.hacs.system.action:
repository.logger.info(f"filename {repository.data.file_name}")
repository.logger.info(f"location {repository.content.path.remote}")
def get_file_name_plugin(repository):
"""Get the filename to target."""
tree = repository.tree
releases = repository.releases.objects
if repository.data.content_in_root:
possible_locations = [""]
else:
possible_locations = ["release", "dist", ""]
# Handler for plug requirement 3
if repository.data.filename:
valid_filenames = [repository.data.filename]
else:
valid_filenames = [
f"{repository.data.name.replace('lovelace-', '')}.js",
f"{repository.data.name}.js",
f"{repository.data.name}.umd.js",
f"{repository.data.name}-bundle.js",
]
for location in possible_locations:
if location == "release":
if not releases:
continue
release = releases[0]
if not release.assets:
continue
asset = release.assets[0]
for filename in valid_filenames:
if filename == asset.name:
repository.data.file_name = filename
repository.content.path.remote = "release"
break
else:
for filename in valid_filenames:
if f"{location+'/' if location else ''}{filename}" in [x.full_path for x in tree]:
repository.data.file_name = filename.split("/")[-1]
repository.content.path.remote = location
break
def get_file_name_integration(repository):
"""Get the filename to target."""
def get_file_name_theme(repository):
"""Get the filename to target."""
tree = repository.tree
for treefile in tree:
if treefile.full_path.startswith(
repository.content.path.remote
) and treefile.full_path.endswith(".yaml"):
repository.data.file_name = treefile.filename
def get_file_name_appdaemon(repository):
"""Get the filename to target."""
def get_file_name_python_script(repository):
"""Get the filename to target."""
tree = repository.tree
for treefile in tree:
if treefile.full_path.startswith(
repository.content.path.remote
) and treefile.full_path.endswith(".py"):
repository.data.file_name = treefile.filename

View File

@@ -0,0 +1,10 @@
"""Helper to check if path is safe to remove."""
from custom_components.hacs.share import get_hacs
from ...utils.path import is_safe
def is_safe_to_remove(path: str) -> bool:
"""Helper to check if path is safe to remove."""
hacs = get_hacs()
return is_safe(hacs, path)

View File

@@ -0,0 +1,35 @@
"""Helper functions: misc"""
import re
from ...utils import version
RE_REPOSITORY = re.compile(
r"(?:(?:.*github.com.)|^)([A-Za-z0-9-]+\/[\w.-]+?)(?:(?:\.git)?|(?:[^\w.-].*)?)$"
)
def get_repository_name(repository) -> str:
"""Return the name of the repository for use in the frontend."""
if repository.repository_manifest.name is not None:
return repository.repository_manifest.name
if repository.data.category == "integration":
if repository.integration_manifest:
if "name" in repository.integration_manifest:
return repository.integration_manifest["name"]
return repository.data.full_name.split("/")[-1].replace("-", " ").replace("_", " ").title()
def version_left_higher_then_right(left: str, right: str) -> bool:
"""Return a bool if source is newer than target, will also be true if identical."""
return version.version_left_higher_then_right(left, right)
def extract_repository_from_url(url: str) -> str or None:
"""Extract the owner/repo part form a URL."""
match = re.match(RE_REPOSITORY, url)
if not match:
return None
return match.group(1).lower()

View File

@@ -0,0 +1,13 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
import os
from custom_components.hacs.share import get_hacs
def path_exsist(path) -> bool:
return os.path.exists(path)
async def async_path_exsist(path) -> bool:
hass = get_hacs().hass
return await hass.async_add_executor_job(path_exsist, path)

View File

@@ -0,0 +1,68 @@
"""Register a repository."""
from __future__ import annotations
from typing import TYPE_CHECKING
from aiogithubapi import AIOGitHubAPIException
from custom_components.hacs.exceptions import (
HacsException,
HacsExpectedException,
HacsRepositoryExistException,
)
from custom_components.hacs.share import get_hacs
from ...repositories import RERPOSITORY_CLASSES
if TYPE_CHECKING:
from ..classes.repository import HacsRepository
# @concurrent(15, 5)
async def register_repository(full_name, category, check=True, ref=None):
"""Register a repository."""
hacs = get_hacs()
if full_name in hacs.common.skip:
if full_name != "hacs/integration":
raise HacsExpectedException(f"Skipping {full_name}")
if category not in RERPOSITORY_CLASSES:
raise HacsException(f"{category} is not a valid repository category.")
repository: HacsRepository = RERPOSITORY_CLASSES[category](full_name)
if check:
try:
await repository.async_registration(ref)
if hacs.status.new:
repository.data.new = False
if repository.validate.errors:
hacs.common.skip.append(repository.data.full_name)
if not hacs.status.startup:
hacs.log.error("Validation for %s failed.", full_name)
if hacs.system.action:
raise HacsException(f"::error:: Validation for {full_name} failed.")
return repository.validate.errors
if hacs.system.action:
repository.logger.info("%s Validation completed", repository)
else:
repository.logger.info("%s Registration completed", repository)
except HacsRepositoryExistException:
return
except AIOGitHubAPIException as exception:
hacs.common.skip.append(repository.data.full_name)
raise HacsException(f"Validation for {full_name} failed with {exception}.") from None
if str(repository.data.id) != "0" and (exists := hacs.get_by_id(repository.data.id)):
hacs.async_remove_repository(exists)
else:
if hacs.hass is not None and ((check and repository.data.new) or hacs.status.new):
hacs.hass.bus.async_fire(
"hacs/repository",
{
"action": "registration",
"repository": repository.data.full_name,
"repository_id": repository.data.id,
},
)
hacs.async_add_repository(repository)

View File

@@ -0,0 +1,50 @@
"""Download."""
import gzip
import os
import shutil
import aiofiles
from custom_components.hacs.utils.logger import getLogger
_LOGGER = getLogger()
async def async_save_file(location, content):
"""Save files."""
_LOGGER.debug("Saving %s", location)
mode = "w"
encoding = "utf-8"
errors = "ignore"
if not isinstance(content, str):
mode = "wb"
encoding = None
errors = None
try:
async with aiofiles.open(location, mode=mode, encoding=encoding, errors=errors) as outfile:
await outfile.write(content)
outfile.close()
# Create gz for .js files
if os.path.isfile(location):
if location.endswith(".js") or location.endswith(".css"):
with open(location, "rb") as f_in:
with gzip.open(location + ".gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
# Remove with 2.0
if "themes" in location and location.endswith(".yaml"):
filename = location.split("/")[-1]
base = location.split("/themes/")[0]
combined = f"{base}/themes/{filename}"
if os.path.exists(combined):
_LOGGER.info("Removing old theme file %s", combined)
os.remove(combined)
except (Exception, BaseException) as error: # pylint: disable=broad-except
_LOGGER.error("Could not write data to %s - %s", location, error)
return False
return os.path.exists(location)

View File

@@ -0,0 +1,79 @@
"""Storage handers."""
# pylint: disable=import-outside-toplevel
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.storage import Store
from homeassistant.util import json as json_util
from custom_components.hacs.const import VERSION_STORAGE
from ...utils.logger import getLogger
_LOGGER = getLogger()
class HACSStore(Store):
"""A subclass of Store that allows multiple loads in the executor."""
def load(self):
"""Load the data from disk if version matches."""
data = json_util.load_json(self.path)
if data == {} or data["version"] != self.version:
return None
return data["data"]
def get_store_key(key):
"""Return the key to use with homeassistant.helpers.storage.Storage."""
return key if "/" in key else f"hacs.{key}"
def _get_store_for_key(hass, key, encoder):
"""Create a Store object for the key."""
return HACSStore(hass, VERSION_STORAGE, get_store_key(key), encoder=encoder)
def get_store_for_key(hass, key):
"""Create a Store object for the key."""
return _get_store_for_key(hass, key, JSONEncoder)
async def async_load_from_store(hass, key):
"""Load the retained data from store and return de-serialized data."""
return await get_store_for_key(hass, key).async_load() or {}
async def async_save_to_store_default_encoder(hass, key, data):
"""Generate store json safe data to the filesystem.
The data is expected to be encodable with the default
python json encoder. It should have already been passed through
JSONEncoder if needed.
"""
await _get_store_for_key(hass, key, None).async_save(data)
async def async_save_to_store(hass, key, data):
"""Generate dynamic data to store and save it to the filesystem.
The data is only written if the content on the disk has changed
by reading the existing content and comparing it.
If the data has changed this will generate two executor jobs
If the data has not changed this will generate one executor job
"""
current = await async_load_from_store(hass, key)
if current is None or current != data:
await get_store_for_key(hass, key).async_save(data)
return
_LOGGER.debug(
"Did not store data for '%s'. Content did not change",
get_store_key(key),
)
async def async_remove_store(hass, key):
"""Remove a store element that should no longer be used."""
if "/" not in key:
return
await get_store_for_key(hass, key).async_remove()

View File

@@ -0,0 +1,32 @@
"""Custom template support."""
# pylint: disable=broad-except
from jinja2 import Template
from custom_components.hacs.utils.logger import getLogger
_LOGGER = getLogger()
def render_template(content, context):
"""Render templates in content."""
# Fix None issues
if context.releases.last_release_object is not None:
prerelease = context.releases.last_release_object.prerelease
else:
prerelease = False
# Render the template
try:
render = Template(content)
render = render.render(
installed=context.data.installed,
pending_update=context.pending_upgrade,
prerelease=prerelease,
selected_tag=context.data.selected_tag,
version_available=context.releases.last_release,
version_installed=context.display_installed_version,
)
return render
except (Exception, BaseException) as exception:
_LOGGER.debug(exception)
return content

View File

@@ -0,0 +1,124 @@
"""Helper to do common validation for repositories."""
from __future__ import annotations
from typing import TYPE_CHECKING
from aiogithubapi import AIOGitHubAPIException
from custom_components.hacs.exceptions import (
HacsException,
HacsNotModifiedException,
HacsRepositoryArchivedException,
HacsRepositoryExistException,
)
from custom_components.hacs.helpers.functions.information import (
get_releases,
get_repository,
get_tree,
)
from custom_components.hacs.helpers.functions.version_to_install import (
version_to_install,
)
from custom_components.hacs.share import get_hacs, is_removed
if TYPE_CHECKING:
from custom_components.hacs.helpers.classes.repository import HacsRepository
async def common_validate(repository, ignore_issues=False):
"""Common validation steps of the repository."""
repository.validate.errors = []
# Make sure the repository exist.
repository.logger.debug("%s Checking repository.", repository)
await common_update_data(repository, ignore_issues)
# Step 6: Get the content of hacs.json
await repository.get_repository_manifest_content()
async def common_update_data(repository: HacsRepository, ignore_issues=False, force=False):
"""Common update data."""
hacs = get_hacs()
releases = []
try:
repository_object, etag = await get_repository(
hacs.session,
hacs.configuration.token,
repository.data.full_name,
etag=None if force or repository.data.installed else repository.data.etag_repository,
)
repository.repository_object = repository_object
if repository.data.full_name.lower() != repository_object.full_name.lower():
hacs.common.renamed_repositories[
repository.data.full_name
] = repository_object.full_name
if str(repository_object.id) not in hacs.common.default:
hacs.common.default.append(str(repository_object.id))
raise HacsRepositoryExistException
repository.data.update_data(repository_object.attributes)
repository.data.etag_repository = etag
except HacsNotModifiedException:
return
except HacsRepositoryExistException:
raise HacsRepositoryExistException from None
except (AIOGitHubAPIException, HacsException) as exception:
if not hacs.status.startup:
repository.logger.error("%s %s", repository, exception)
if not ignore_issues:
repository.validate.errors.append("Repository does not exist.")
raise HacsException(exception) from None
# Make sure the repository is not archived.
if repository.data.archived and not ignore_issues:
repository.validate.errors.append("Repository is archived.")
hacs.common.archived_repositories.append(repository.data.full_name)
raise HacsRepositoryArchivedException("Repository is archived.")
# Make sure the repository is not in the blacklist.
if is_removed(repository.data.full_name) and not ignore_issues:
repository.validate.errors.append("Repository is in the blacklist.")
raise HacsException("Repository is in the blacklist.")
# Get releases.
try:
releases = await get_releases(
repository.repository_object,
repository.data.show_beta,
hacs.configuration.release_limit,
)
if releases:
repository.data.releases = True
repository.releases.objects = [x for x in releases if not x.draft]
repository.data.published_tags = [x.tag_name for x in repository.releases.objects]
repository.data.last_version = next(iter(repository.data.published_tags))
except (AIOGitHubAPIException, HacsException):
repository.data.releases = False
if not repository.force_branch:
repository.ref = version_to_install(repository)
if repository.data.releases:
for release in repository.releases.objects or []:
if release.tag_name == repository.ref:
assets = release.assets
if assets:
downloads = next(iter(assets)).attributes.get("download_count")
repository.data.downloads = downloads
repository.logger.debug(
"%s Running checks against %s", repository, repository.ref.replace("tags/", "")
)
try:
repository.tree = await get_tree(repository.repository_object, repository.ref)
if not repository.tree:
raise HacsException("No files in tree")
repository.treefiles = []
for treefile in repository.tree:
repository.treefiles.append(treefile.full_path)
except (AIOGitHubAPIException, HacsException) as exception:
if not hacs.status.startup:
repository.logger.error("%s %s", repository, exception)
if not ignore_issues:
raise HacsException(exception) from None

View File

@@ -0,0 +1,20 @@
"""Install helper for repositories."""
def version_to_install(repository):
"""Determine which version to isntall."""
if repository.data.last_version is not None:
if repository.data.selected_tag is not None:
if repository.data.selected_tag == repository.data.last_version:
repository.data.selected_tag = None
return repository.data.last_version
return repository.data.selected_tag
return repository.data.last_version
if repository.data.selected_tag is not None:
if repository.data.selected_tag == repository.data.default_branch:
return repository.data.default_branch
if repository.data.selected_tag in repository.data.published_tags:
return repository.data.selected_tag
if repository.data.default_branch is None:
return "main"
return repository.data.default_branch

View File

@@ -0,0 +1,30 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from custom_components.hacs.helpers.methods.installation import (
RepositoryMethodInstall,
RepositoryMethodPostInstall,
RepositoryMethodPreInstall,
)
from custom_components.hacs.helpers.methods.registration import (
RepositoryMethodPostRegistration,
RepositoryMethodPreRegistration,
RepositoryMethodRegistration,
)
from custom_components.hacs.helpers.methods.reinstall_if_needed import (
RepositoryMethodReinstallIfNeeded,
)
class RepositoryHelperMethods(
RepositoryMethodReinstallIfNeeded,
RepositoryMethodInstall,
RepositoryMethodPostInstall,
RepositoryMethodPreInstall,
RepositoryMethodPreRegistration,
RepositoryMethodRegistration,
RepositoryMethodPostRegistration,
):
"""Collection of repository methods that are nested to all repositories."""
class HacsHelperMethods:
"""Helper class for HACS methods"""

View File

@@ -0,0 +1,113 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from abc import ABC
import os
import tempfile
from custom_components.hacs.exceptions import HacsException
from custom_components.hacs.helpers.functions.download import download_content
from custom_components.hacs.helpers.functions.version_to_install import (
version_to_install,
)
from custom_components.hacs.operational.backup import Backup, BackupNetDaemon
from custom_components.hacs.share import get_hacs
class RepositoryMethodPreInstall(ABC):
async def async_pre_install(self) -> None:
pass
async def _async_pre_install(self) -> None:
self.logger.info("Running pre installation steps")
await self.async_pre_install()
self.logger.info("Pre installation steps completed")
class RepositoryMethodInstall(ABC):
async def async_install(self) -> None:
await self._async_pre_install()
self.logger.info("Running installation steps")
await async_install_repository(self)
self.logger.info("Installation steps completed")
await self._async_post_install()
class RepositoryMethodPostInstall(ABC):
async def async_post_installation(self) -> None:
pass
async def _async_post_install(self) -> None:
self.logger.info("Running post installation steps")
await self.async_post_installation()
self.data.new = False
self.hacs.hass.bus.async_fire(
"hacs/repository",
{"id": 1337, "action": "install", "repository": self.data.full_name},
)
self.logger.info("Post installation steps completed")
async def async_install_repository(repository):
"""Common installation steps of the repository."""
hacs = get_hacs()
persistent_directory = None
await repository.update_repository()
if repository.content.path.local is None:
raise HacsException("repository.content.path.local is None")
repository.validate.errors = []
if not repository.can_install:
raise HacsException("The version of Home Assistant is not compatible with this version")
version = version_to_install(repository)
if version == repository.data.default_branch:
repository.ref = version
else:
repository.ref = f"tags/{version}"
if repository.data.installed and repository.data.category == "netdaemon":
persistent_directory = await hacs.hass.async_add_executor_job(BackupNetDaemon, repository)
await hacs.hass.async_add_executor_job(persistent_directory.create)
elif repository.data.persistent_directory:
if os.path.exists(
f"{repository.content.path.local}/{repository.data.persistent_directory}"
):
persistent_directory = Backup(
f"{repository.content.path.local}/{repository.data.persistent_directory}",
tempfile.gettempdir() + "/hacs_persistent_directory/",
)
await hacs.hass.async_add_executor_job(persistent_directory.create)
if repository.data.installed and not repository.content.single:
backup = Backup(repository.content.path.local)
await hacs.hass.async_add_executor_job(backup.create)
if repository.data.zip_release and version != repository.data.default_branch:
await repository.download_zip_files(repository.validate)
else:
await download_content(repository)
if repository.validate.errors:
for error in repository.validate.errors:
repository.logger.error(error)
if repository.data.installed and not repository.content.single:
await hacs.hass.async_add_executor_job(backup.restore)
if repository.data.installed and not repository.content.single:
await hacs.hass.async_add_executor_job(backup.cleanup)
if persistent_directory is not None:
await hacs.hass.async_add_executor_job(persistent_directory.restore)
await hacs.hass.async_add_executor_job(persistent_directory.cleanup)
if repository.validate.success:
if repository.data.full_name not in repository.hacs.common.installed:
if repository.data.full_name == "hacs/integration":
repository.hacs.common.installed.append(repository.data.full_name)
repository.data.installed = True
repository.data.installed_commit = repository.data.last_commit
if version == repository.data.default_branch:
repository.data.installed_version = None
else:
repository.data.installed_version = version

View File

@@ -0,0 +1,41 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member, attribute-defined-outside-init
from abc import ABC
from custom_components.hacs.validate import async_run_repository_checks
class RepositoryMethodPreRegistration(ABC):
async def async_pre_registration(self):
pass
class RepositoryMethodRegistration(ABC):
async def registration(self, ref=None) -> None:
self.logger.warning("'registration' is deprecated, use 'async_registration' instead")
await self.async_registration(ref)
async def async_registration(self, ref=None) -> None:
# Run local pre registration steps.
await self.async_pre_registration()
if ref is not None:
self.data.selected_tag = ref
self.ref = ref
self.force_branch = True
if not await self.validate_repository():
return False
# Run common registration steps.
await self.common_registration()
# Set correct local path
self.content.path.local = self.localpath
# Run local post registration steps.
await self.async_post_registration()
class RepositoryMethodPostRegistration(ABC):
async def async_post_registration(self):
await async_run_repository_checks(self)

View File

@@ -0,0 +1,12 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from abc import ABC
from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist
class RepositoryMethodReinstallIfNeeded(ABC):
async def async_reinstall_if_needed(self) -> None:
if self.data.installed:
if not await async_path_exsist(self.content.path.local):
self.logger.error("Missing from local FS, should be reinstalled.")
# await self.async_install()

View File

@@ -0,0 +1,16 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from custom_components.hacs.helpers.properties.can_be_installed import (
RepositoryPropertyCanBeInstalled,
)
from custom_components.hacs.helpers.properties.custom import RepositoryPropertyCustom
from custom_components.hacs.helpers.properties.pending_update import (
RepositoryPropertyPendingUpdate,
)
class RepositoryHelperProperties(
RepositoryPropertyPendingUpdate,
RepositoryPropertyCustom,
RepositoryPropertyCanBeInstalled,
):
pass

View File

@@ -0,0 +1,21 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from abc import ABC
from custom_components.hacs.helpers.functions.misc import version_left_higher_then_right
class RepositoryPropertyCanBeInstalled(ABC):
@property
def can_be_installed(self) -> bool:
if self.data.homeassistant is not None:
if self.data.releases:
if not version_left_higher_then_right(
self.hacs.core.ha_version, self.data.homeassistant
):
return False
return True
@property
def can_install(self):
"""kept for legacy compatibility"""
return self.can_be_installed

View File

@@ -0,0 +1,13 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from abc import ABC
class RepositoryPropertyCustom(ABC):
@property
def custom(self):
"""Return flag if the repository is custom."""
if str(self.data.id) in self.hacs.common.default:
return False
if self.data.full_name == "hacs/integration":
return False
return True

View File

@@ -0,0 +1,23 @@
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member
from abc import ABC
class RepositoryPropertyPendingUpdate(ABC):
@property
def pending_update(self) -> bool:
if not self.can_install:
return False
if self.data.installed:
if self.data.selected_tag is not None:
if self.data.selected_tag == self.data.default_branch:
if self.data.installed_commit != self.data.last_commit:
return True
return False
if self.display_installed_version != self.display_available_version:
return True
return False
@property
def pending_upgrade(self) -> bool:
"""kept for legacy compatibility"""
return self.pending_update

View File

@@ -0,0 +1,29 @@
const hacsIcons = {
hacs: {
path: "m 20.064849,22.306912 c -0.0319,0.369835 -0.280561,0.707789 -0.656773,0.918212 -0.280572,0.153036 -0.605773,0.229553 -0.950094,0.229553 -0.0765,0 -0.146661,-0.0064 -0.216801,-0.01275 -0.605774,-0.05739 -1.135016,-0.344329 -1.402827,-0.7588 l 0.784304,-0.516495 c 0.0893,0.146659 0.344331,0.312448 0.707793,0.34433 0.235931,0.02551 0.471852,-0.01913 0.637643,-0.108401 0.101998,-0.05101 0.172171,-0.127529 0.17854,-0.191295 0.0065,-0.08289 -0.0255,-0.369835 -0.733293,-0.439975 -1.013854,-0.09565 -1.645127,-0.688661 -1.568606,-1.460214 0.0319,-0.382589 0.280561,-0.714165 0.663153,-0.930965 0.331571,-0.172165 0.752423,-0.25506 1.166895,-0.210424 0.599382,0.05739 1.128635,0.344329 1.402816,0.7588 l -0.784304,0.510118 c -0.0893,-0.140282 -0.344331,-0.299694 -0.707782,-0.331576 -0.235932,-0.02551 -0.471863,0.01913 -0.637654,0.10202 -0.0956,0.05739 -0.165791,0.133906 -0.17216,0.191295 -0.0255,0.293317 0.465482,0.420847 0.726913,0.439976 v 0.0064 c 1.020234,0.09565 1.638757,0.66953 1.562237,1.460213 z m -7.466854,-0.988354 c 0,-1.192401 0.962855,-2.155249 2.15525,-2.155249 0.599393,0 1.179645,0.25506 1.594117,0.707789 l -0.695033,0.624895 c -0.235931,-0.25506 -0.561133,-0.401718 -0.899084,-0.401718 -0.675903,0 -1.217906,0.542 -1.217906,1.217906 0,0.66953 0.542003,1.217908 1.217906,1.217908 0.337951,0 0.663153,-0.140283 0.899084,-0.401718 l 0.695033,0.631271 c -0.414472,0.452729 -0.988355,0.707788 -1.594117,0.707788 -1.192395,0 -2.15525,-0.969224 -2.15525,-2.148872 z M 8.6573365,23.461054 10.353474,19.14418 h 0.624893 l 1.568618,4.316874 H 11.52037 L 11.265308,22.734136 H 9.964513 l -0.274192,0.726918 z m 1.6833885,-1.68339 h 0.580263 L 10.646796,21.012487 Z M 8.1089536,19.156932 v 4.297745 H 7.1461095 v -1.645131 h -1.606867 v 1.645131 H 4.5763876 v -4.297745 h 0.9628549 v 1.696143 h 1.606867 V 19.156932 Z M 20.115859,4.2997436 C 20.090359,4.159461 19.969198,4.0574375 19.822548,4.0574375 H 14.141102 10.506516 4.8250686 c -0.14665,0 -0.2678112,0.1020202 -0.2933108,0.2423061 L 3.690064,8.8461703 c -0.00651,0.01913 -0.00651,0.03826 -0.00651,0.057391 v 1.5239797 c 0,0.165789 0.133911,0.299694 0.2996911,0.299694 H 4.5762579 20.0711 20.664112 c 0.165781,0 0.299691,-0.133905 0.299691,-0.299694 V 8.8971848 c 0,-0.01913 0,-0.03826 -0.0065,-0.05739 z M 4.5763876,17.358767 c 0,0.184917 0.1466608,0.331577 0.3315819,0.331577 h 5.5985465 3.634586 0.924594 c 0.184911,0 0.331571,-0.14666 0.331571,-0.331577 v -4.744098 c 0,-0.184918 0.146661,-0.331577 0.331582,-0.331577 h 2.894913 c 0.184921,0 0.331582,0.146659 0.331582,0.331577 v 4.744098 c 0,0.184917 0.146661,0.331577 0.331571,0.331577 h 0.446363 c 0.18491,0 0.331571,-0.14666 0.331571,-0.331577 v -5.636804 c 0,-0.184918 -0.146661,-0.331577 -0.331571,-0.331577 H 4.9079695 c -0.1849211,0 -0.3315819,0.146659 -0.3315819,0.331577 z m 1.6578879,-4.852498 h 5.6495565 c 0.15303,0 0.280561,0.12753 0.280561,0.280564 v 3.513438 c 0,0.153036 -0.127531,0.280566 -0.280561,0.280566 H 6.2342755 c -0.1530412,0 -0.2805719,-0.12753 -0.2805719,-0.280566 v -3.513438 c 0,-0.159411 0.1275307,-0.280564 0.2805719,-0.280564 z M 19.790657,3.3879075 H 4.8569594 c -0.1530412,0 -0.2805718,-0.1275296 -0.2805718,-0.2805642 V 1.3665653 C 4.5763876,1.2135296 4.7039182,1.086 4.8569594,1.086 H 19.790657 c 0.153041,0 0.280572,0.1275296 0.280572,0.2805653 v 1.740778 c 0,0.1530346 -0.127531,0.2805642 -0.280572,0.2805642 z",
keywords: ["hacs", "home assistant community store"],
},
};
window.customIcons = window.customIcons || {};
window.customIconsets = window.customIconsets || {};
// For Home Assistant > 2021.11
window.customIcons["hacs"] = {
getIcon: async (iconName) => (
{ path: hacsIcons[iconName]?.path }
),
getIconList: async () =>
Object.entries(hacsIcons).map(([icon, content]) => ({
name: icon,
keywords: content.keywords,
})
)
};
// For Home Assistant < 2021.11
window.customIconsets["hacs"] = async () => {
return {
path: hacsIcons.hacs.path
};
};

View File

@@ -0,0 +1,27 @@
{
"codeowners": [
"@ludeeus"
],
"config_flow": true,
"dependencies": [
"http",
"websocket_api",
"frontend",
"persistent_notification",
"lovelace"
],
"documentation": "https://hacs.xyz/docs/configuration/start",
"domain": "hacs",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/hacs/integration/issues",
"name": "HACS",
"requirements": [
"aiofiles>=0.6.0",
"aiogithubapi>=21.8.1",
"awesomeversion>=21.2.2",
"backoff>=1.10.0",
"hacs_frontend==20211010111104",
"queueman==0.5"
],
"version": "1.16.0"
}

View File

@@ -0,0 +1,24 @@
"""Mixin classes."""
# pylint: disable=too-few-public-methods
from __future__ import annotations
from logging import Logger
from typing import TYPE_CHECKING
from .share import get_hacs
from .utils.logger import getLogger
if TYPE_CHECKING:
from .hacsbase.hacs import Hacs
class HacsMixin:
"""Mixin to provide 'self.hacs' to classes."""
hacs: Hacs = get_hacs()
class LogMixin:
"""Mixin to provide 'self.log' to classes."""
log: Logger = getLogger()

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