Compare commits

..

9 Commits

Author SHA1 Message Date
6081c238a9 fix for immich api change 2024-09-04 21:46:30 +02:00
694db084ed small fixes 2024-06-29 12:48:55 +02:00
e05196ff83 update to reflect immich api changes 2024-03-03 13:15:50 +01:00
d3a641e1f8 Improved setup and deploy 2023-12-20 16:00:01 +01:00
58d318faee now with notify 2023-11-21 23:45:29 +01:00
3d27a0ee04 better color handling 2023-11-20 14:41:40 +01:00
08bdfe970c added other sources for more variety 2023-11-17 22:25:55 +01:00
b684c293b0 Refactor get process to add multiple sources later on 2023-11-14 17:06:31 +01:00
2fb48c494b cleaner test logic 2023-11-08 22:26:01 +01:00
19 changed files with 556 additions and 243 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
keys.py keys.py
*.pyc *.pyc
.image-cache/ .cache/
test.png test.png
wifi_networks.yml wifi_networks.yml
program_variables.yml

View File

@@ -6,6 +6,7 @@ name = "pypi"
[packages] [packages]
pillow = "*" pillow = "*"
httpx = "*" httpx = "*"
pyyaml = "*"
[dev-packages] [dev-packages]

220
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "67ce1ab808a1bcb6013a54e3d675786aaa8852f6a8a94d3df78c600503cc1e2a" "sha256": "8fd64e8e7662ff58ccf2dc70efbc2cedb3e07283b6e791eaa7bc410172505306"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -18,19 +18,19 @@
"default": { "default": {
"anyio": { "anyio": {
"hashes": [ "hashes": [
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8",
"sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a" "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.0.0" "version": "==4.3.0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2023.7.22" "version": "==2024.2.2"
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
@@ -42,97 +42,169 @@
}, },
"httpcore": { "httpcore": {
"hashes": [ "hashes": [
"sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9", "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73",
"sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced" "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.18.0" "version": "==1.0.4"
}, },
"httpx": { "httpx": {
"hashes": [ "hashes": [
"sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100", "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
"sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875" "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.25.0" "version": "==0.27.0"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==3.4" "version": "==3.6"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8",
"sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39",
"sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac",
"sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869",
"sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e",
"sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04",
"sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9",
"sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e",
"sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe",
"sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef",
"sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56",
"sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa",
"sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f",
"sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f",
"sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e",
"sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a",
"sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2",
"sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2",
"sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5",
"sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a",
"sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2",
"sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213",
"sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563",
"sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591",
"sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c",
"sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2",
"sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb",
"sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757",
"sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0",
"sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452",
"sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad",
"sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01",
"sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f",
"sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5",
"sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61",
"sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e",
"sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b",
"sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068",
"sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9",
"sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588",
"sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483",
"sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f",
"sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67",
"sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7",
"sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311",
"sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6",
"sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72",
"sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6",
"sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129",
"sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13",
"sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67",
"sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c",
"sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516",
"sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e",
"sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e",
"sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364",
"sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023",
"sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1",
"sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04",
"sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d",
"sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a",
"sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7",
"sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb",
"sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4",
"sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e",
"sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1",
"sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48",
"sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==10.1.0" "version": "==10.2.0"
},
"pyyaml": {
"hashes": [
"sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
"sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
"sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
"sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
"sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
"sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
"sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
"sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
"sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
"sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
"sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
"sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
"sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
"sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
"sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
"sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
"sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
"sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
"sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
"sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
"sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
"sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
"sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
"sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
"sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
"sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
"sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
"sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
"sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
"sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
"sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
"sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
"sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
"sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
"sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
"sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
"sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
"sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
"sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
"sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
"sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
"sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
"sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
"sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
"sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
"sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
"sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
"sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
"sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==6.0.1"
}, },
"sniffio": { "sniffio": {
"hashes": [ "hashes": [
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==1.3.0" "version": "==1.3.1"
} }
}, },
"develop": {}, "develop": {},

9
README
View File

@@ -2,7 +2,10 @@
## Deployment ## Deployment
Run the ansible playbooks (`setup` once, `deploy` on each code update): Run the ansible playbooks (`setup` once, `deploy` on each code update):
(Don't forget to set variables!)
``` ```
ansible-playbook deploy/setup.playbook.yml -i <ip of pi> ansible-playbook deploy/setup.playbook.yml -i <ip of pi>,
ansible-playbook deploy/deploy.playbook.yml -i <ip of pi> ansible-playbook deploy/deploy.playbook.yml -i <ip of pi>,
``` ```
Note the trailing comma in the `-i` flag. This is required for ansible to parse the ip as a list.

View File

@@ -4,14 +4,21 @@
# become: true # become: true
vars: vars:
code_dest: /home/remy/eink # mostly fixed
repo_dest: /home/remy/eink
code_dest: /home/remy/eink/src
service_target_dir: /etc/systemd/system/ service_target_dir: /etc/systemd/system/
tasks: tasks:
- name: Load program variables
ansible.builtin.include_vars:
file: ./program_variables.yml
name: program_variables
- name: Pull the latest version of the code - name: Pull the latest version of the code
git: git:
repo: https://git.kluster.moll.re/remoll/rpi-eink-picture-frame repo: https://git.kluster.moll.re/remoll/rpi-eink-picture-frame
dest: "{{ code_dest }}" dest: "{{ repo_dest }}"
version: main version: main
- name: Install pillow dependencies - name: Install pillow dependencies
@@ -21,16 +28,11 @@
- zlib1g - zlib1g
state: present state: present
become: true become: true
- name: Install from the pipenv-file - name: Install from the pipenv-file
command: "pipenv install --system --deploy --categories=\"packages prod-packages\"" command: "pipenv install --system --deploy --categories=\"packages prod-packages\""
args: args:
chdir: "{{ code_dest }}" chdir: "{{ repo_dest }}"
- name: Copy keys python file
copy:
src: ../keys.py
dest: "{{ code_dest }}/keys.py"
- name: Copy unit files - name: Copy unit files
template: template:
@@ -50,4 +52,4 @@
loop: loop:
- eink-show.service - eink-show.service
- eink-show.timer - eink-show.timer
become: true become: true

View File

@@ -4,19 +4,20 @@
become: true become: true
tasks: tasks:
# DONE BY RPI IMAGER
# - name: Add local ssh key
# ansible.posix.authorized_key:
# user: "{{ ansible_user }}"
# state: present
# # copy file present on the controller to the remote host
# key: "{{ lookup('file', '~/.ssh/default.pub') }}"
- name: Include network variables - name: Include network variables
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: wifi_networks.yml file: wifi_networks.yml
name: wifi_external name: wifi_external
- name: Use raspi-config to set WIFI country
shell: |
raspi-config nonint do_wifi_country CH
- name: Use raspi-config to set WIFI SSID and password
shell: |
raspi-config nonint do_wifi_ssid_passphrase "{{ item.ssid }}" "{{ item.password }}"
loop: "{{ wifi_external.networks }}"
- name: apt update all packages - name: apt update all packages
apt: apt:
update_cache: yes update_cache: yes
@@ -52,26 +53,3 @@
reboot: reboot:
connect_timeout: 20 connect_timeout: 20
reboot_timeout: 300 reboot_timeout: 300
- name: Wait for the Raspberry Pi to reboot
wait_for_connection:
delay: 5
timeout: 300
# - name: Add wifi networks from template
# template:
# src: templates/wpa_supplicant.conf.j2
# dest: /etc/wpa_supplicant/wpa_supplicant.conf
# owner: root
# group: root
# mode: 0600
- name: Use raspi-config to set WIFI country
shell: |
raspi-config nonint do_wifi_country CH
- name: Use raspi-config to set WIFI SSID and password
shell: |
raspi-config nonint do_wifi_ssid_passphrase "{{ item.ssid }}" "{{ item.password }}"
loop: "{{ wifi_external.networks }}"

View File

@@ -5,8 +5,15 @@ Description=Show new photo onto the eink display
Type=oneshot Type=oneshot
User={{ ansible_user }} User={{ ansible_user }}
WorkingDirectory={{ code_dest }} WorkingDirectory={{ code_dest }}
ExecStartPre=/bin/sleep 30
ExecStart=python main.py ExecStart=python main.py
Environment="OWN_IMAGE_PERCENT={{ program_variables.OWN_IMAGE_PERCENT }}"
Environment="RECIPIENT={{ program_variables.RECIPIENT }}"
Environment="IMMICH_API_URL={{ program_variables.IMMICH_API_URL }}"
Environment="IMMICH_API_KEY={{ program_variables.IMMICH_API_KEY }}"
Environment="IMMICH_ALBUM_ID={{ program_variables.IMMICH_ALBUM_ID }}"
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
# this ensures that the service is started after the network is up (@reboot)

View File

@@ -2,7 +2,7 @@
Description=Run photo update regularly Description=Run photo update regularly
[Timer] [Timer]
OnCalendar=08:00 OnCalendar=7:30
OnCalendar=16:00 OnCalendar=16:00
Persistent=true Persistent=true

View File

@@ -1,94 +0,0 @@
import httpx as h
import json
import random
from pathlib import Path
import uuid
import keys
class ImageGetException(Exception):
pass
class ImageGetter:
headers = {
"x-api-key": keys.immich_api_key
}
cached_num = 10
cached_path = Path("./.image-cache/")
def get_random_image(self) -> bytearray:
try:
id = self.get_random_image_ids()[0]
bytes = self.get_image_file(id)
self.save_cached_files()
except (h.ConnectError, h.HTTPStatusError, h.NetworkError, h.RequestError, h.DecodingError, h.TransportError, ImageGetException):
print("Loading image from cache")
bytes = self.load_cached_file()
return bytes
def get_random_image_ids(self, num=1) -> str:
url = keys.immich_api_root_url + "album/" + keys.immich_album_id
headers = self.headers | {"Accept": "application/json"}
response = h.request("GET", url, headers=headers, data={})
# raises an htppx exception if anything goes wrong
if response.status_code == 200:
response = json.loads(response.text)
else:
raise ImageGetException("Error in step get_random_image_id: " + str(response.status_code))
images = response['assets']
print(f"Picking {num} random id(s) out of {len(images)} album images")
ids = []
for i in range(num):
image = random.choice(images)
print(f"Image considered: {image['exifInfo']}")
ids.append(image["id"])
return ids
def get_image_file(self, image_id: str) -> bytearray:
url = keys.immich_api_root_url + "asset/download/" + image_id
headers = self.headers | {"Accept": "application/octet-stream"}
response = h.request("POST", url, headers=headers, data={})
if not response.status_code == 200:
raise ImageGetException("Error in step get_image_file: " + str(response.status_code))
return response.content
def save_cached_files(self) -> None:
"""Ensures self.cached_num files are at self.cached_path at any time"""
if not self.cached_path.exists():
self.cached_path.mkdir()
present_count = len(list(self.cached_path.glob("*")))
missing = self.cached_num - present_count
if missing == 0:
return
ids = self.get_random_image_ids(missing)
for i, id in enumerate(ids):
print(f"Caching image {i + 1}")
new_cache = self.cached_path / f"{uuid.uuid4()}"
new_cache.write_bytes(self.get_image_file(id))
def load_cached_file(self) -> bytearray:
"""Returns a random file from self.cached_path"""
files = list(self.cached_path.glob("*"))
if len(files) == 0:
raise ImageGetException("Could not load cached file: directory empty")
file = random.choice(files)
bytes = file.read_bytes()
file.unlink()
return bytes

20
main.py
View File

@@ -1,20 +0,0 @@
from image_convert import ImageShrink
from image_get import ImageGetter
try:
from image_show import ImageShow
show_image = True
except ImportError:
print("ImageShow not found. Image will not be displayed.")
show_image = False
get = ImageGetter()
convert = ImageShrink()
if show_image:
show = ImageShow()
image = get.get_random_image()
image = convert.convert(image)
if show_image:
show.show_image(image)
else:
print("Would have shown image now.")

25
src/get/combined.py Normal file
View File

@@ -0,0 +1,25 @@
from .immich import ImageGetImmich
from .unsplash import ImageGetUnsplash
from .museum import ImageGetMuseum
import random
import os
class ImageGetCombined:
def __init__(self) -> None:
igetter = ImageGetImmich()
ugettter = ImageGetUnsplash()
mgetter = ImageGetMuseum()
own_image_percent = int(os.getenv("OWN_IMAGE_PERCENT", 90))
other_image_percent = int(100 - own_image_percent)
self.getters = [igetter] * own_image_percent \
+ [ugettter] * int(other_image_percent/2) \
+ [mgetter] * int(other_image_percent/2)
# representing weighted probabilities
print(f"Using 3 image sources: ~{own_image_percent}% own, ~{other_image_percent}% foreign images.")
def get_random_image(self) -> bytearray:
getter = random.choice(self.getters)
return getter.get_random_image()

57
src/get/immich.py Normal file
View File

@@ -0,0 +1,57 @@
import httpx as h
import random
import json
import os
from .template import ImageGet, ImageGetException
class ImageGetImmich(ImageGet):
def __init__(self) -> None:
api_root_url = os.getenv("IMMICH_API_URL")
api_key = os.getenv("IMMICH_API_KEY")
headers = {
"x-api-key": api_key
}
super().__init__(
base_url=api_root_url,
headers=headers
)
def get_random_image_ids(self, num=1) -> str:
album_id = os.getenv("IMMICH_ALBUM_ID")
url = self.base_url + "albums/" + album_id
headers = self.headers | {"Accept": "application/json"}
response = h.request("GET", url, headers=headers, data={})
# raises an htppx exception if anything goes wrong
if response.status_code == 200:
response = json.loads(response.text)
else:
raise ImageGetException(f"Error in step get_random_image_id: {response.status_code}")
images = response['assets']
print(f"Picking {num} random id(s) out of {len(images)} album images")
ids = []
for i in range(num):
image = random.choice(images)
print(f"Image considered: {image['exifInfo']}")
ids.append(image["id"])
return ids
def get_image_file(self, image_id: str) -> bytearray:
url = self.base_url + f"assets/{image_id}/original"
print(url)
headers = self.headers | {"Accept": "application/octet-stream"}
response = h.request("GET", url, headers=headers, data={})
if not response.status_code == 200:
raise ImageGetException(f"Error in step get_image_file: {response.status_code}" )
return response.content

68
src/get/museum.py Normal file
View File

@@ -0,0 +1,68 @@
import httpx as h
import random
import json
from .template import ImageGet, ImageGetException
class ImageGetMuseum(ImageGet):
def __init__(self) -> None:
headers = {
"AIC-User-Agent": "Eink Art Display (remoll@ethz.ch)"
}
super().__init__(
base_url="https://api.artic.edu/api/v1/artworks/",
headers=headers
)
def get_random_image_ids(self, num=1) -> list[str]:
post_data = {
"resources": "artworks",
"fields": ["id","image_id"],
"boost": False,
"limit": num,
"query": {
"function_score": {
"query": {
"bool": {
"filter": [
{"term": {"is_public_domain": True}},
{"exists": {"field": "image_id"}}
]
}
},
"boost_mode": "replace",
"random_score": {
"field": "id",
"seed": f"{random.randint(1, 1000000)}"
}
}
}
}
response = h.post(self.base_url + "search", json=post_data, headers=self.headers)
if not response.status_code == 200:
raise ImageGetException("Error in step get_random_image_ids: " + str(response.status_code))
response = json.loads(response.text)
images = response["data"]
ids = [img["id"] for img in images]
return ids
def get_image_file(self, image_id: str) -> bytearray:
url = self.base_url + f"{image_id}"
print(url)
response = h.get(url, headers=self.headers)
if not response.status_code == 200:
raise ImageGetException("Error in step get_image_file: " + str(response.status_code))
response = json.loads(response.text)
image_url = response["config"]["iiif_url"] + f"/{response['data']['image_id']}/full/843,/0/default.jpg"
try:
image = h.get(image_url).content
except (h.ConnectError, h.NetworkError, h.RequestError, h.HTTPStatusError) as e:
raise ImageGetException(f"Error in step get_image_file: {e}")
return image

72
src/get/template.py Normal file
View File

@@ -0,0 +1,72 @@
import httpx as h
import random
from pathlib import Path
import uuid
class ImageGetException(Exception):
pass
class ImageGet:
def __init__(self, base_url, headers, cache_num = 10) -> None:
self.base_url = base_url
self.headers = headers
self.cache_num = cache_num
self.cache_dir = Path(f"../.cache/{self.__class__.__name__}/")
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.save_cached_files()
# Main entrypoint, called externally
def get_random_image(self) -> bytearray:
try:
id = self.get_random_image_ids()[0]
bytes = self.get_image_file(id)
self.save_cached_files()
except (h.ConnectError, h.HTTPStatusError, h.NetworkError, h.RequestError, h.DecodingError, h.TransportError, ImageGetException) as e:
print(f"Encoutered {e}, loading image from cache")
bytes = self.load_cached_file()
return bytes
def save_cached_files(self) -> None:
"""Ensures self.cache_num files are at self.cache_dir at any time"""
present_count = len(list(self.cache_dir.glob("*")))
missing = self.cache_num - present_count
if missing == 0:
return
ids = self.get_random_image_ids(num = missing)
for i, id in enumerate(ids):
print(f"Caching image {i + 1}")
new_cache = self.cache_dir / f"{uuid.uuid4()}"
try:
new_cache.write_bytes(self.get_image_file(id))
except ImageGetException as e:
print(f"Could not cache image, skipping ({e})")
continue
def load_cached_file(self) -> bytearray:
"""Returns a random file from self.cache_dir"""
files = list(self.cache_dir.glob("*"))
if len(files) == 0:
raise ImageGetException("Could not load cached file: directory empty")
file = random.choice(files)
bytes = file.read_bytes()
file.unlink()
return bytes
def get_random_image_ids(self, num=1) -> list[str]:
raise NotImplementedError
def get_image_file(self, image_id: str) -> bytearray:
raise NotImplementedError

27
src/get/unsplash.py Normal file
View File

@@ -0,0 +1,27 @@
import httpx as h
from .template import ImageGet, ImageGetException
class ImageGetUnsplash(ImageGet):
def __init__(self) -> None:
super().__init__(
base_url="https://source.unsplash.com/",
headers={}
)
def get_random_image_ids(self, num=1) -> list[str]:
# there are no ids since the api has a random endpoint
return [0 for i in range(num)]
def get_image_file(self, image_id: str = "doesntmatter") -> bytearray:
url = self.base_url + "random"
response = h.get(url, follow_redirects=True)
if not response.status_code == 200:
raise ImageGetException("Error in step get_image_file: " + str(response.status_code))
return response.content

View File

@@ -1,6 +1,5 @@
from PIL import Image, ImageOps from PIL import Image, ImageOps
import io import io
import os
black_base = [0, 0, 0] black_base = [0, 0, 0]
white_base = [255, 255, 255] white_base = [255, 255, 255]
@@ -33,16 +32,15 @@ class ImageShrink:
"""Shrinks a given image (bytearray) to a given resolution (width, height)""" """Shrinks a given image (bytearray) to a given resolution (width, height)"""
resolution = (480, 800) resolution = (480, 800)
def __init__(self) -> None: def __init__(self, reduce_colors=False) -> None:
pass self.reduce_colors = reduce_colors
def convert(self, image: bytearray) -> Image: def convert(self, image: bytearray) -> Image:
# load image from bytearray # load image from bytearray
image = Image.open(io.BytesIO(image)) image = Image.open(io.BytesIO(image))
image = self.shrink(image) image = self.shrink(image)
image = self.convert_to_reduced_colors(image) image = self.convert_to_reduced_colors(image)
if os.uname().machine == "x86_64":
image.save("test.png")
return image return image
@@ -62,5 +60,13 @@ class ImageShrink:
if image.mode != "RGB": if image.mode != "RGB":
print("Converting image to RGB") print("Converting image to RGB")
image = image.convert("RGB") image = image.convert("RGB")
new_image = image.quantize(colors = len(palette), palette=ref_image, dither=True) if not self.reduce_colors:
print("Not manually reducing colors")
return image
new_image = image.quantize(
colors = len(palette),
palette = ref_image,
dither = True
)
return new_image return new_image

View File

@@ -1,5 +1,37 @@
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
import epaper try:
import epaper
except ModuleNotFoundError:
# generate dummy class for testing
class epaper:
class epaper:
def __init__(self, name):
pass
class EPD:
width = 480
height = 800
WHITE = 0
BLUE = 0
ORANGE = 0
BLACK = 0
GREEN = 0
RED = 0
YELLOW = 0
def __init__(self):
pass
def init(self):
pass
def Clear(self):
pass
def display(self, image):
print("Dummy output")
def sleep(self):
pass
def getbuffer(self, image:Image):
image.save("test.png")
return image
# BLACK = 0x000000 # 0000 BGR # BLACK = 0x000000 # 0000 BGR
# WHITE = 0xffffff # 0001 # WHITE = 0xffffff # 0001
# GREEN = 0x00ff00 # 0010 # GREEN = 0x00ff00 # 0010

50
src/main.py Normal file
View File

@@ -0,0 +1,50 @@
import sys
from pathlib import Path
import os
from image_convert import ImageShrink
from get.combined import ImageGetCombined
from get.template import ImageGetException
from image_show import ImageShow
import notify
VARS = Path("../deploy/program_variables.yml")
if os.getenv("IMMICH_API_URL") is None:
# values are not set, import them from local yaml file
import yaml
with open(VARS, "r") as f:
config = yaml.safe_load(f)
for key, value in config.items():
os.environ[key] = value
if len(sys.argv) == 2 and sys.argv[1] == "test":
print("Running test")
shower = ImageShow()
shower.draw_sample_image()
sys.exit()
shrink_kwargs = {}
if "reduce" in sys.argv:
print("Enabling color reduction")
shrink_kwargs["reduce_colors"] = True
try:
getter = ImageGetCombined()
converter = ImageShrink(**shrink_kwargs)
shower = ImageShow()
except Exception as e:
print("Failed to initialize")
notify.notify_status("Failed to initialize")
raise e
try:
image = getter.get_random_image()
image = converter.convert(image)
shower.show_image(image)
notify.notify_status("Success")
except ImageGetException as e:
print("Failed to get image")
notify.notify_status(f"Failed to get image: {e}")
raise e

26
src/notify.py Normal file
View File

@@ -0,0 +1,26 @@
import httpx as h
import os
import socket
RECIPIENT = os.getenv("RECIPIENT", "DEV")
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(('8.8.8.8', 80))
(addr, port) = s.getsockname()
IPS = addr
def notify_status(status):
data = f"Status: {status}\nIP: {IPS}"
headers = {
"Title" : f"Eink - {RECIPIENT}",
# "Click": "https://home.nest.com/",
# "Attach": "https://nest.com/view/yAxkasd.jpg",
# "Actions": "http, Open door, https://api.nest.com/open/yAxkasd, clear=true",
# "Email": "phil@example.com"
}
h.post(
"https://ntfy.kluster.moll.re/eink",
data=data,
headers=headers
)