├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── AUTHORS.md ├── LICENSE ├── README.md ├── cli ├── cron.ts ├── runthrough.ts └── setlight.ts ├── deno.json ├── deno.lock └── src ├── calendar.ts ├── colours.ts ├── libs.ts ├── light.ts ├── pharus.ts ├── state.ts ├── sunrise.ts ├── today.ts └── vigil.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .*swp 2 | *.pyc 3 | *.pyo 4 | .DS_Store 5 | __pycache__ 6 | *.egg-info 7 | .ropeproject 8 | build/ 9 | dist/ 10 | .idea/ 11 | .tox 12 | .cache 13 | .vscode/ 14 | node_modules/ 15 | *.tgz 16 | npm/ 17 | tmp/ 18 | config.json 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | public 3 | *.json 4 | *.md 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Authors: 2 | 3 | Fr Grahame Bowland 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pharus: 'beacon' or 'lighthouse' 2 | 3 | https://github.com/grahame/pharus/assets/330805/0644480d-e9b8-4c07-a9b1-3218ce54215d 4 | 5 | _`pharus` showing the colours of the calendar, Lent through Easter 2024_ 6 | 7 | This software allows you to build a 'liturgical lightbulb', which changes colour 8 | in accordance with the Calendar of the Anglican Church of Australia. 9 | 10 | The various festivals in the Calendar are observed, including lesser feasts. Where more than one lesser feast might be celebrated on a day, the feasts are rotated each year, so that every few years every feast 11 | in the calendar will be observed. 12 | 13 | As a solemn touch, your 'liturgical lightbulb' will turn off at 3pm local time on Good Friday. 14 | It will turn back on at sunrise on Easter Day. 15 | 16 | ## Building a liturgical lightbulb 17 | 18 | Setting this project up will require some Linux skills. It could be a great hobby project though; 19 | in my own area, I've been considering running a workshop where attendees set up a light, 20 | and leave with it. 21 | 22 | 1. You'll need a working [zigbee2mqtt](https://www.zigbee2mqtt.io/guide/getting-started/) setup. This isn't too hard, but it does involve ordering some hardware. The walkthrough is good. In terms of controllers, I've got an [Electrolama CC2652R1](https://shop.electrolama.com/collections/usb-rf-sticks/products/zzh-multiprotocol-rf-stick?variant=40387937468577) plugged into a [Raspberry Pi](https://www.raspberrypi.org/) and it has worked well. 23 | 2. Get yourself a lightbulb, and register it with your Zigbee controller. I've got an [Tradfri LED bulb E14 470 lumen, dimmable colour](https://www.ikea.com/au/en/p/tradfri-led-bulb-e14-470-lumen-smart-wireless-dimmable-colour-and-white-spectrum-globe-50439197/) globe. 24 | 3. Make yourself a configuration file: 25 | 26 | ```json 27 | { 28 | "url": "mqtt://127.0.0.1", 29 | "topic": "zigbee2mqtt/0x2c1165fffe107f64/set", 30 | "latitude": -31.9514, 31 | "longitude": 115.8617 32 | } 33 | ``` 34 | 35 | You can find the ID in the topic for your lightbulb from the zigbee2mqtt web interface; 36 | it's just the hex code in the device URL. 37 | 38 | The provided latitude and longitude are used to calculate the local sunrise time on Easter Day, so that the globe turns back on as the Vigil ends. 39 | 40 | 4. Now for the easy bit. Install the [deno](https://docs.deno.com/runtime/manual/getting_started/installation) runtime, and then run: 41 | 42 | ```sh 43 | # test things out by setting the globe to today's colour 44 | deno run -A https://deno.land/x/pharus/cli/setlight.ts config.json 45 | 46 | # leave this running, and your globe will change day by day 47 | deno run --unstable -A https://deno.land/x/pharus/cli/cron.ts config.json 48 | 49 | # ... or this will run your light through the calendar for the next year 50 | # changing the colour twice a second 51 | deno run --unstable -A https://deno.land/x/pharus/cli/runthrough.ts config.json 52 | ``` 53 | 54 | ## Background 55 | 56 | This project came out of my days as a theological student, training for ordination as an Anglican priest. 57 | One of the requirements for ordination in my Diocese is completion of [Clinical Pastoral Education](https://www.acpewa.org/). 58 | For three months I was an intern chaplain at Royal Perth Hospital. It was a very intense experience: overall, 59 | wonderful, but complex in the detail. I found myself spending some time in [St Mary's Cathedral](https://stmaryscathedralperth.com.au/) 60 | when I needed to, and noticed that they change the lighting around the sanctuary day by day according to the Calendar. 61 | 62 | I decided to take that idea and bring it into my home. Changing the colour of a light each day was going to be 63 | too much work for me to keep it up, so I decided to automate it. I built the light as a little fun distraction while 64 | completing CPE; this version of the light is much more polished. (The original used an IKEA Tradfri controller and some 65 | Python code to talk to it, but it kept breaking when the controller updated itself; this is version 2, and is pretty solid!) 66 | 67 | While this started as a fun project, the liturgical light has added meaningfully to my pattern of daily worship. When I get 68 | up in the morning, one of the first things I see is the colour of the day, coming through the study door. 69 | When it's red, or white, I find myself wondering which saint or martyr is marked this day. 70 | 71 | The initial release of this software was made on the Feast of the Epiphany, 2024. I'm not sure there 72 | could be a much more appropriate day for that to happen! 73 | 74 | ## Future directions 75 | 76 | At the moment, the lightglobe follows the Calendar of the Anglican Church of Australia. 77 | It does this using my [church-calendar](https://github.com/grahame/church-calendar) library. 78 | I am very open to adding support for further calendars, including those from other denominations, 79 | or other provinces within the Anglican Communion. 80 | 81 | ## Need a hand? 82 | 83 | I'm happy to help anyone who is trying to get this running and hits obstacles. Just open an issue on this 84 | repository. 85 | 86 | If you do get this up and running, I'd love to hear from you. I'm an Anglican Priest working in Digital Mission, and I hope this has some missional outworkings. 87 | -------------------------------------------------------------------------------- /cli/cron.ts: -------------------------------------------------------------------------------- 1 | import { watchLights } from "../src/light.ts"; 2 | import { Config, pharusApply } from "../src/pharus.ts"; 3 | import { State } from "../src/state.ts"; 4 | import { get_now } from "../src/today.ts"; 5 | 6 | const state: State = { 7 | context: { valid: false }, 8 | }; 9 | 10 | // update the light every minute 11 | Deno.cron("Sample cron job", "*/1 * * * *", async () => { 12 | const configFile = Deno.args[0]; 13 | const config: Config = JSON.parse(await Deno.readTextFile(configFile)); 14 | const now = get_now(new Date()); 15 | state.context = await pharusApply(config, state.context, now); 16 | if (state.context.valid && state.context.wasChanged) { 17 | console.log(JSON.stringify(state.context)); 18 | } 19 | }); 20 | 21 | // we also want to update the light immediately when it comes online, 22 | // so we subscribe to availability 23 | const main = async (configFile: string) => { 24 | const config: Config = JSON.parse(await Deno.readTextFile(configFile)); 25 | 26 | await watchLights( 27 | config.url, 28 | config.lights, 29 | async (light, availability) => { 30 | console.log("availability change:", light, availability); 31 | const now = get_now(new Date()); 32 | state.context = await pharusApply( 33 | config, 34 | { ...state.context, valid: false }, 35 | now 36 | ); 37 | } 38 | ); 39 | }; 40 | 41 | await main(Deno.args[0]); 42 | -------------------------------------------------------------------------------- /cli/runthrough.ts: -------------------------------------------------------------------------------- 1 | import { State } from "../src/state.ts"; 2 | import { Config, pharusApply } from "../src/pharus.ts"; 3 | import { get_now } from "../src/today.ts"; 4 | import { Temporal } from "../src/libs.ts"; 5 | 6 | const state: State = { 7 | context: { valid: false }, 8 | }; 9 | 10 | const tick = async (config: Config, now: Temporal.PlainDateTime) => { 11 | state.context = await pharusApply(config, state.context, now); 12 | if (state.context.valid && state.context.wasChanged) { 13 | console.log(JSON.stringify(state.context)); 14 | } 15 | }; 16 | 17 | const main = async (configFile: string) => { 18 | const config: Config = JSON.parse(await Deno.readTextFile(configFile)); 19 | // run through the calendar for the next year or so 20 | const now = get_now(new Date()); 21 | const until = now.add({ years: 1 }); 22 | let then = now; 23 | 24 | while (Temporal.PlainDateTime.compare(then, until) < 0) { 25 | await tick(config, then); 26 | if (state.context.valid && state.context.wasChanged) { 27 | await new Promise((r) => setTimeout(r, 500)); 28 | } 29 | then = then.add({ days: 1 }); 30 | } 31 | }; 32 | 33 | await main(Deno.args[0]); 34 | -------------------------------------------------------------------------------- /cli/setlight.ts: -------------------------------------------------------------------------------- 1 | import { Config, pharusApply } from "../src/pharus.ts"; 2 | import { get_now } from "../src/today.ts"; 3 | 4 | const main = async (configFile: string) => { 5 | const config: Config = JSON.parse(await Deno.readTextFile(configFile)); 6 | const now = get_now(new Date()); 7 | pharusApply(config, { valid: false }, now); 8 | }; 9 | 10 | await main(Deno.args[0]); 11 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "dev": "deno run --watch main.ts" 4 | }, 5 | "imports": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "packages": { 4 | "specifiers": { 5 | "npm:@js-temporal/polyfill": "npm:@js-temporal/polyfill@0.4.4", 6 | "npm:@js-temporal/polyfill@0.4.4": "npm:@js-temporal/polyfill@0.4.4", 7 | "npm:sunrise-sunset-js": "npm:sunrise-sunset-js@2.2.1" 8 | }, 9 | "npm": { 10 | "@js-temporal/polyfill@0.4.4": { 11 | "integrity": "sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==", 12 | "dependencies": { 13 | "jsbi": "jsbi@4.3.0", 14 | "tslib": "tslib@2.6.2" 15 | } 16 | }, 17 | "jsbi@4.3.0": { 18 | "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==", 19 | "dependencies": {} 20 | }, 21 | "sunrise-sunset-js@2.2.1": { 22 | "integrity": "sha512-ErsvmxoTCZRacVPtlchkrTAR8qxypBy0BDrrv9LMugLuF0AykcS5pQsP1EhQJHgumxrTTSI8N8KJkQMVJ6dEPw==", 23 | "dependencies": {} 24 | }, 25 | "tslib@2.6.2": { 26 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", 27 | "dependencies": {} 28 | } 29 | } 30 | }, 31 | "redirects": { 32 | "https://deno.land/x/mqtt/deno/mod.ts": "https://deno.land/x/mqtt@0.1.2/deno/mod.ts" 33 | }, 34 | "remote": { 35 | "https://deno.land/x/churchcalendar@1.4.4/calendar.ts": "7417b61f64aa42d83cbd349e4a0dd09872b7ff72d2096f00e64410cde0493283", 36 | "https://deno.land/x/churchcalendar@1.4.4/festivalindex.ts": "2768e77e8ffc7123fa04582ed88a053bcb61635f4bbf0a4552035310d0f16dbe", 37 | "https://deno.land/x/churchcalendar@1.4.4/nth.ts": "37968ad256fdf616301e5a592b7091f4236910cb0c01ed6e9226b8e195aa5201", 38 | "https://deno.land/x/churchcalendar@1.4.4/packing.ts": "11be9534a198978b5a2a9d2ee2da9d940b69ee0462640a5e8c84ad7fd0034547", 39 | "https://deno.land/x/churchcalendar@1.4.4/temporal.ts": "447b81ea34a4f19aae82b13ac264f1cea3da12613020f801fbf562503de70eaa", 40 | "https://deno.land/x/churchcalendar@1.4.4/western/calendars/anglican-church-of-australia.ts": "898761efc42b574fa9657bdf064b7fa8e9d6687bd6a3efe7a58633e09b184a2f", 41 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/advent.ts": "2908030b0683238617db55e3b1f9042171156f929abf152a4c211052730461d6", 42 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/christmas.ts": "7d4a97e82eb219069754d4b2b2229da9a3138dc6ceb13543ed67070ea4c27a75", 43 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/easter.ts": "d580d0ef69937ec53879d4396c859748e8a79bd7580753bb065f4d3a0f5cb4d8", 44 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/epiphany.ts": "6a5424ae8e8ccd3dfe8809246604326b42bfb0667e1d831f7cd1b67b92109c12", 45 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/feasts.ts": "2840045b213e012c021f54e4eec38f0c2f43ef21f3e0aeb07b8d52223749273d", 46 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/holy_week.ts": "0bf1b605ff383f9c5dfdd1203a0da5b2b96929c8cd716e9b519839733af2a42e", 47 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/index.ts": "b10b5ef4d9f2569febc06e0067e6f035f53a72d17fbd4a9f48042d5e572fce22", 48 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/lent.ts": "90e8014ff0bd02e61613a5c3fe378fe2f154797e21033319e4dd76bfe538fd94", 49 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/lesser_festivals.ts": "15ee81aa50f8e7c04c32aed1078971ab59a181441aaf39312b983f0a294eb18c", 50 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/pentecost.ts": "3c2d1a02e823c0bdd0e5129aaeebe8fc499779c39e222671f8149d907d3ad2be", 51 | "https://deno.land/x/churchcalendar@1.4.4/western/festivals/triduum.ts": "196436dc7baafb0adbde97d70fa31b36229e02a912fc11efb7750f248af0d15b", 52 | "https://deno.land/x/churchcalendar@1.4.4/western/liturgicalyear.ts": "a6045095b2b29769fdc79ed367135c810b9895204543cd2626d0913160b2dabe", 53 | "https://deno.land/x/churchcalendar@1.4.4/western/seasons/advent/advent.ts": "4844b83c6915296690b0e3ee579c9aeacd1b5cf756517e9228766240efd5f2b9", 54 | "https://deno.land/x/churchcalendar@1.4.4/western/seasons/easter/easter.ts": "9da3f1237f05f123631aa8f47f685b0bbbb754f0c732e148017b59c2d106cf44", 55 | "https://deno.land/x/churchcalendar@1.4.4/western/seasons/index.ts": "8a5bc88399a58a0b30372c5faa66ffd2de7b943b252e06e81070c656052521b5", 56 | "https://deno.land/x/churchcalendar@1.4.4/western/seasons/pentecost/pentecost.ts": "ab7c1d8851e286b5445abb7e46152b7ab7390d1d4ea6c3b4496eb3d7911fa13c", 57 | "https://deno.land/x/churchcalendar@1.4.4/western/sunday.ts": "ea0be3f63f35cdd5792da0b88b51db0a456cc639d0c7e0834cee24b8e34ba0f6", 58 | "https://deno.land/x/churchcalendar@1.4.5/calendar.ts": "7417b61f64aa42d83cbd349e4a0dd09872b7ff72d2096f00e64410cde0493283", 59 | "https://deno.land/x/churchcalendar@1.4.5/festivalindex.ts": "2768e77e8ffc7123fa04582ed88a053bcb61635f4bbf0a4552035310d0f16dbe", 60 | "https://deno.land/x/churchcalendar@1.4.5/nth.ts": "37968ad256fdf616301e5a592b7091f4236910cb0c01ed6e9226b8e195aa5201", 61 | "https://deno.land/x/churchcalendar@1.4.5/packing.ts": "11be9534a198978b5a2a9d2ee2da9d940b69ee0462640a5e8c84ad7fd0034547", 62 | "https://deno.land/x/churchcalendar@1.4.5/temporal.ts": "447b81ea34a4f19aae82b13ac264f1cea3da12613020f801fbf562503de70eaa", 63 | "https://deno.land/x/churchcalendar@1.4.5/western/calendars/anglican-church-of-australia.ts": "898761efc42b574fa9657bdf064b7fa8e9d6687bd6a3efe7a58633e09b184a2f", 64 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/advent.ts": "2908030b0683238617db55e3b1f9042171156f929abf152a4c211052730461d6", 65 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/christmas.ts": "7d4a97e82eb219069754d4b2b2229da9a3138dc6ceb13543ed67070ea4c27a75", 66 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/easter.ts": "d580d0ef69937ec53879d4396c859748e8a79bd7580753bb065f4d3a0f5cb4d8", 67 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/epiphany.ts": "5c778cb6277e1b7ea1ecec69f12329e8927ef74d92b7161dc9b4e816dbdc5510", 68 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/feasts.ts": "2840045b213e012c021f54e4eec38f0c2f43ef21f3e0aeb07b8d52223749273d", 69 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/holy_week.ts": "0bf1b605ff383f9c5dfdd1203a0da5b2b96929c8cd716e9b519839733af2a42e", 70 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/index.ts": "b10b5ef4d9f2569febc06e0067e6f035f53a72d17fbd4a9f48042d5e572fce22", 71 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/lent.ts": "90e8014ff0bd02e61613a5c3fe378fe2f154797e21033319e4dd76bfe538fd94", 72 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/lesser_festivals.ts": "15ee81aa50f8e7c04c32aed1078971ab59a181441aaf39312b983f0a294eb18c", 73 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/pentecost.ts": "a7219a0f2bc30b2df458e4b28579ecc43ba899dd6f4e90780ce25212c467d4ec", 74 | "https://deno.land/x/churchcalendar@1.4.5/western/festivals/triduum.ts": "196436dc7baafb0adbde97d70fa31b36229e02a912fc11efb7750f248af0d15b", 75 | "https://deno.land/x/churchcalendar@1.4.5/western/liturgicalyear.ts": "a6045095b2b29769fdc79ed367135c810b9895204543cd2626d0913160b2dabe", 76 | "https://deno.land/x/churchcalendar@1.4.5/western/seasons/advent/advent.ts": "4844b83c6915296690b0e3ee579c9aeacd1b5cf756517e9228766240efd5f2b9", 77 | "https://deno.land/x/churchcalendar@1.4.5/western/seasons/easter/easter.ts": "9da3f1237f05f123631aa8f47f685b0bbbb754f0c732e148017b59c2d106cf44", 78 | "https://deno.land/x/churchcalendar@1.4.5/western/seasons/index.ts": "8a5bc88399a58a0b30372c5faa66ffd2de7b943b252e06e81070c656052521b5", 79 | "https://deno.land/x/churchcalendar@1.4.5/western/seasons/pentecost/pentecost.ts": "ab7c1d8851e286b5445abb7e46152b7ab7390d1d4ea6c3b4496eb3d7911fa13c", 80 | "https://deno.land/x/churchcalendar@1.4.5/western/sunday.ts": "ea0be3f63f35cdd5792da0b88b51db0a456cc639d0c7e0834cee24b8e34ba0f6", 81 | "https://deno.land/x/churchcalendar@1.5.0/calendar.ts": "e24a829e67acfe92a1bc5ed41f6ff89390ab3acaca0cc43926caed6057e58324", 82 | "https://deno.land/x/churchcalendar@1.5.0/festivalindex.ts": "2768e77e8ffc7123fa04582ed88a053bcb61635f4bbf0a4552035310d0f16dbe", 83 | "https://deno.land/x/churchcalendar@1.5.0/nth.ts": "37968ad256fdf616301e5a592b7091f4236910cb0c01ed6e9226b8e195aa5201", 84 | "https://deno.land/x/churchcalendar@1.5.0/packing.ts": "3c62a42691497db5a789648e118056eaaaa65d4c8dafba7c533118b0ed2d4186", 85 | "https://deno.land/x/churchcalendar@1.5.0/temporal.ts": "447b81ea34a4f19aae82b13ac264f1cea3da12613020f801fbf562503de70eaa", 86 | "https://deno.land/x/churchcalendar@1.5.0/western/calendars/anglican-church-of-australia.ts": "2acdd778c3a6539a0b1125d3b2e8cf16bf9125bbefaf4c56c57abaed4096d571", 87 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/advent.ts": "2908030b0683238617db55e3b1f9042171156f929abf152a4c211052730461d6", 88 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/christmas.ts": "7d4a97e82eb219069754d4b2b2229da9a3138dc6ceb13543ed67070ea4c27a75", 89 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/easter.ts": "d580d0ef69937ec53879d4396c859748e8a79bd7580753bb065f4d3a0f5cb4d8", 90 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/epiphany.ts": "5c778cb6277e1b7ea1ecec69f12329e8927ef74d92b7161dc9b4e816dbdc5510", 91 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/feasts.ts": "2840045b213e012c021f54e4eec38f0c2f43ef21f3e0aeb07b8d52223749273d", 92 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/holy_week.ts": "0bf1b605ff383f9c5dfdd1203a0da5b2b96929c8cd716e9b519839733af2a42e", 93 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/index.ts": "b10b5ef4d9f2569febc06e0067e6f035f53a72d17fbd4a9f48042d5e572fce22", 94 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/lent.ts": "90e8014ff0bd02e61613a5c3fe378fe2f154797e21033319e4dd76bfe538fd94", 95 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/lesser_festivals.ts": "460d3eefa72d15763a1a94a02cfaa26affc3244d0422a511929f73206255e470", 96 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/pentecost.ts": "a7219a0f2bc30b2df458e4b28579ecc43ba899dd6f4e90780ce25212c467d4ec", 97 | "https://deno.land/x/churchcalendar@1.5.0/western/festivals/triduum.ts": "196436dc7baafb0adbde97d70fa31b36229e02a912fc11efb7750f248af0d15b", 98 | "https://deno.land/x/churchcalendar@1.5.0/western/liturgicalyear.ts": "a6045095b2b29769fdc79ed367135c810b9895204543cd2626d0913160b2dabe", 99 | "https://deno.land/x/churchcalendar@1.5.0/western/seasons/advent/advent.ts": "4844b83c6915296690b0e3ee579c9aeacd1b5cf756517e9228766240efd5f2b9", 100 | "https://deno.land/x/churchcalendar@1.5.0/western/seasons/easter/easter.ts": "9da3f1237f05f123631aa8f47f685b0bbbb754f0c732e148017b59c2d106cf44", 101 | "https://deno.land/x/churchcalendar@1.5.0/western/seasons/index.ts": "8a5bc88399a58a0b30372c5faa66ffd2de7b943b252e06e81070c656052521b5", 102 | "https://deno.land/x/churchcalendar@1.5.0/western/seasons/pentecost/pentecost.ts": "ab7c1d8851e286b5445abb7e46152b7ab7390d1d4ea6c3b4496eb3d7911fa13c", 103 | "https://deno.land/x/churchcalendar@1.5.0/western/sunday.ts": "ea0be3f63f35cdd5792da0b88b51db0a456cc639d0c7e0834cee24b8e34ba0f6", 104 | "https://deno.land/x/churchcalendar@1.5.2/calendar.ts": "ad469e2164ee47f8d4a1f7ce0ba5ca6d350dd8f2241dc4131408a988ff36397a", 105 | "https://deno.land/x/churchcalendar@1.5.2/festivalindex.ts": "2768e77e8ffc7123fa04582ed88a053bcb61635f4bbf0a4552035310d0f16dbe", 106 | "https://deno.land/x/churchcalendar@1.5.2/nth.ts": "37968ad256fdf616301e5a592b7091f4236910cb0c01ed6e9226b8e195aa5201", 107 | "https://deno.land/x/churchcalendar@1.5.2/packing.ts": "3c62a42691497db5a789648e118056eaaaa65d4c8dafba7c533118b0ed2d4186", 108 | "https://deno.land/x/churchcalendar@1.5.2/temporal.ts": "447b81ea34a4f19aae82b13ac264f1cea3da12613020f801fbf562503de70eaa", 109 | "https://deno.land/x/churchcalendar@1.5.2/western/calendars/anglican-church-of-australia.ts": "db2fd6b7e24855359740ee11eed53f99f5bb6e02ef6a531244c6080dc3aa350f", 110 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/advent.ts": "2908030b0683238617db55e3b1f9042171156f929abf152a4c211052730461d6", 111 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/christmas.ts": "7d4a97e82eb219069754d4b2b2229da9a3138dc6ceb13543ed67070ea4c27a75", 112 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/easter.ts": "d580d0ef69937ec53879d4396c859748e8a79bd7580753bb065f4d3a0f5cb4d8", 113 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/epiphany.ts": "5c778cb6277e1b7ea1ecec69f12329e8927ef74d92b7161dc9b4e816dbdc5510", 114 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/feasts.ts": "2840045b213e012c021f54e4eec38f0c2f43ef21f3e0aeb07b8d52223749273d", 115 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/holy_week.ts": "0bf1b605ff383f9c5dfdd1203a0da5b2b96929c8cd716e9b519839733af2a42e", 116 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/index.ts": "b10b5ef4d9f2569febc06e0067e6f035f53a72d17fbd4a9f48042d5e572fce22", 117 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/lent.ts": "90e8014ff0bd02e61613a5c3fe378fe2f154797e21033319e4dd76bfe538fd94", 118 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/lesser_festivals.ts": "460d3eefa72d15763a1a94a02cfaa26affc3244d0422a511929f73206255e470", 119 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/pentecost.ts": "a7219a0f2bc30b2df458e4b28579ecc43ba899dd6f4e90780ce25212c467d4ec", 120 | "https://deno.land/x/churchcalendar@1.5.2/western/festivals/triduum.ts": "196436dc7baafb0adbde97d70fa31b36229e02a912fc11efb7750f248af0d15b", 121 | "https://deno.land/x/churchcalendar@1.5.2/western/liturgicalyear.ts": "a6045095b2b29769fdc79ed367135c810b9895204543cd2626d0913160b2dabe", 122 | "https://deno.land/x/churchcalendar@1.5.2/western/seasons/advent/advent.ts": "4844b83c6915296690b0e3ee579c9aeacd1b5cf756517e9228766240efd5f2b9", 123 | "https://deno.land/x/churchcalendar@1.5.2/western/seasons/easter/easter.ts": "9da3f1237f05f123631aa8f47f685b0bbbb754f0c732e148017b59c2d106cf44", 124 | "https://deno.land/x/churchcalendar@1.5.2/western/seasons/index.ts": "8a5bc88399a58a0b30372c5faa66ffd2de7b943b252e06e81070c656052521b5", 125 | "https://deno.land/x/churchcalendar@1.5.2/western/seasons/pentecost/pentecost.ts": "ab7c1d8851e286b5445abb7e46152b7ab7390d1d4ea6c3b4496eb3d7911fa13c", 126 | "https://deno.land/x/churchcalendar@1.5.2/western/sunday.ts": "ea0be3f63f35cdd5792da0b88b51db0a456cc639d0c7e0834cee24b8e34ba0f6", 127 | "https://deno.land/x/churchcalendar@1.5.3/calendar.ts": "ad469e2164ee47f8d4a1f7ce0ba5ca6d350dd8f2241dc4131408a988ff36397a", 128 | "https://deno.land/x/churchcalendar@1.5.3/festivalindex.ts": "2768e77e8ffc7123fa04582ed88a053bcb61635f4bbf0a4552035310d0f16dbe", 129 | "https://deno.land/x/churchcalendar@1.5.3/nth.ts": "37968ad256fdf616301e5a592b7091f4236910cb0c01ed6e9226b8e195aa5201", 130 | "https://deno.land/x/churchcalendar@1.5.3/packing.ts": "3c62a42691497db5a789648e118056eaaaa65d4c8dafba7c533118b0ed2d4186", 131 | "https://deno.land/x/churchcalendar@1.5.3/temporal.ts": "447b81ea34a4f19aae82b13ac264f1cea3da12613020f801fbf562503de70eaa", 132 | "https://deno.land/x/churchcalendar@1.5.3/western/calendars/anglican-church-of-australia.ts": "db2fd6b7e24855359740ee11eed53f99f5bb6e02ef6a531244c6080dc3aa350f", 133 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/advent.ts": "2908030b0683238617db55e3b1f9042171156f929abf152a4c211052730461d6", 134 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/christmas.ts": "7d4a97e82eb219069754d4b2b2229da9a3138dc6ceb13543ed67070ea4c27a75", 135 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/easter.ts": "d580d0ef69937ec53879d4396c859748e8a79bd7580753bb065f4d3a0f5cb4d8", 136 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/epiphany.ts": "5c778cb6277e1b7ea1ecec69f12329e8927ef74d92b7161dc9b4e816dbdc5510", 137 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/feasts.ts": "2840045b213e012c021f54e4eec38f0c2f43ef21f3e0aeb07b8d52223749273d", 138 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/holy_week.ts": "0bf1b605ff383f9c5dfdd1203a0da5b2b96929c8cd716e9b519839733af2a42e", 139 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/index.ts": "b10b5ef4d9f2569febc06e0067e6f035f53a72d17fbd4a9f48042d5e572fce22", 140 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/lent.ts": "90e8014ff0bd02e61613a5c3fe378fe2f154797e21033319e4dd76bfe538fd94", 141 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/lesser_festivals.ts": "340330f06ada94d08f76f6088f4eb56f57e9480e8b94c19645094fd17a30d095", 142 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/pentecost.ts": "9020fbbf52a420ebf8f962a8005c46dec3b9d6926025cc30c18b2235e0a1359b", 143 | "https://deno.land/x/churchcalendar@1.5.3/western/festivals/triduum.ts": "196436dc7baafb0adbde97d70fa31b36229e02a912fc11efb7750f248af0d15b", 144 | "https://deno.land/x/churchcalendar@1.5.3/western/liturgicalyear.ts": "a6045095b2b29769fdc79ed367135c810b9895204543cd2626d0913160b2dabe", 145 | "https://deno.land/x/churchcalendar@1.5.3/western/seasons/advent/advent.ts": "4844b83c6915296690b0e3ee579c9aeacd1b5cf756517e9228766240efd5f2b9", 146 | "https://deno.land/x/churchcalendar@1.5.3/western/seasons/easter/easter.ts": "9da3f1237f05f123631aa8f47f685b0bbbb754f0c732e148017b59c2d106cf44", 147 | "https://deno.land/x/churchcalendar@1.5.3/western/seasons/index.ts": "8a5bc88399a58a0b30372c5faa66ffd2de7b943b252e06e81070c656052521b5", 148 | "https://deno.land/x/churchcalendar@1.5.3/western/seasons/pentecost/pentecost.ts": "ab7c1d8851e286b5445abb7e46152b7ab7390d1d4ea6c3b4496eb3d7911fa13c", 149 | "https://deno.land/x/churchcalendar@1.5.3/western/sunday.ts": "ea0be3f63f35cdd5792da0b88b51db0a456cc639d0c7e0834cee24b8e34ba0f6", 150 | "https://deno.land/x/churchcalendar@1.5.4/calendar.ts": "ad469e2164ee47f8d4a1f7ce0ba5ca6d350dd8f2241dc4131408a988ff36397a", 151 | "https://deno.land/x/churchcalendar@1.5.4/festivalindex.ts": "2768e77e8ffc7123fa04582ed88a053bcb61635f4bbf0a4552035310d0f16dbe", 152 | "https://deno.land/x/churchcalendar@1.5.4/nth.ts": "37968ad256fdf616301e5a592b7091f4236910cb0c01ed6e9226b8e195aa5201", 153 | "https://deno.land/x/churchcalendar@1.5.4/packing.ts": "3c62a42691497db5a789648e118056eaaaa65d4c8dafba7c533118b0ed2d4186", 154 | "https://deno.land/x/churchcalendar@1.5.4/temporal.ts": "447b81ea34a4f19aae82b13ac264f1cea3da12613020f801fbf562503de70eaa", 155 | "https://deno.land/x/churchcalendar@1.5.4/western/calendars/anglican-church-of-australia.ts": "db2fd6b7e24855359740ee11eed53f99f5bb6e02ef6a531244c6080dc3aa350f", 156 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/advent.ts": "2908030b0683238617db55e3b1f9042171156f929abf152a4c211052730461d6", 157 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/christmas.ts": "7d4a97e82eb219069754d4b2b2229da9a3138dc6ceb13543ed67070ea4c27a75", 158 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/easter.ts": "d580d0ef69937ec53879d4396c859748e8a79bd7580753bb065f4d3a0f5cb4d8", 159 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/epiphany.ts": "5c778cb6277e1b7ea1ecec69f12329e8927ef74d92b7161dc9b4e816dbdc5510", 160 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/feasts.ts": "2840045b213e012c021f54e4eec38f0c2f43ef21f3e0aeb07b8d52223749273d", 161 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/holy_week.ts": "0bf1b605ff383f9c5dfdd1203a0da5b2b96929c8cd716e9b519839733af2a42e", 162 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/index.ts": "b10b5ef4d9f2569febc06e0067e6f035f53a72d17fbd4a9f48042d5e572fce22", 163 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/lent.ts": "90e8014ff0bd02e61613a5c3fe378fe2f154797e21033319e4dd76bfe538fd94", 164 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/lesser_festivals.ts": "b8b08e73fb4e1e2c444a4dd1ed9af4132a4487c1933b8f9fe520e94872a2a836", 165 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/pentecost.ts": "9020fbbf52a420ebf8f962a8005c46dec3b9d6926025cc30c18b2235e0a1359b", 166 | "https://deno.land/x/churchcalendar@1.5.4/western/festivals/triduum.ts": "196436dc7baafb0adbde97d70fa31b36229e02a912fc11efb7750f248af0d15b", 167 | "https://deno.land/x/churchcalendar@1.5.4/western/liturgicalyear.ts": "a6045095b2b29769fdc79ed367135c810b9895204543cd2626d0913160b2dabe", 168 | "https://deno.land/x/churchcalendar@1.5.4/western/seasons/advent/advent.ts": "4844b83c6915296690b0e3ee579c9aeacd1b5cf756517e9228766240efd5f2b9", 169 | "https://deno.land/x/churchcalendar@1.5.4/western/seasons/easter/easter.ts": "9da3f1237f05f123631aa8f47f685b0bbbb754f0c732e148017b59c2d106cf44", 170 | "https://deno.land/x/churchcalendar@1.5.4/western/seasons/index.ts": "8a5bc88399a58a0b30372c5faa66ffd2de7b943b252e06e81070c656052521b5", 171 | "https://deno.land/x/churchcalendar@1.5.4/western/seasons/pentecost/pentecost.ts": "ab7c1d8851e286b5445abb7e46152b7ab7390d1d4ea6c3b4496eb3d7911fa13c", 172 | "https://deno.land/x/churchcalendar@1.5.4/western/sunday.ts": "ea0be3f63f35cdd5792da0b88b51db0a456cc639d0c7e0834cee24b8e34ba0f6", 173 | "https://deno.land/x/mqtt@0.1.2/client/base_client.ts": "c6fbd5e327a9d06628e4c5bfeb1881c5d97cdc9fb01ebc9bef726ddc0ad10ced", 174 | "https://deno.land/x/mqtt@0.1.2/deno/deno_client.ts": "39b6ef2008107604965654374ebd58c679dcd6d941e0efb4e0a74b20eb283f46", 175 | "https://deno.land/x/mqtt@0.1.2/deno/mod.ts": "dcb37ab97d47432d984a41729d4f440cdb2f3ae20cce7525474fc24433f12ce3", 176 | "https://deno.land/x/mqtt@0.1.2/lib/mod.ts": "863c871f3d0b6e925067dc80e8fffc7e09addc7095e0f4489409687ee9081032", 177 | "https://deno.land/x/mqtt@0.1.2/packets/connack.ts": "d3e9ada09d812a3c0be1d4d321008ba36feb1ea56933fbdd406e0ea1671e16d1", 178 | "https://deno.land/x/mqtt@0.1.2/packets/connect.ts": "2088ec4eab48ffd95061dcf3aaf3c298451b96bb43e6aa6f061f10da960909ea", 179 | "https://deno.land/x/mqtt@0.1.2/packets/disconnect.ts": "8d1088e88459e3463b9b4fa63060e6b21dc2cc97677d1622b95c6aad3427f826", 180 | "https://deno.land/x/mqtt@0.1.2/packets/length.ts": "975d38ae7beb920cbfd682901e9d4eeeb54fa61854d0addd593bf646065d7ce6", 181 | "https://deno.land/x/mqtt@0.1.2/packets/mod.ts": "f25b831c818914b1770c7826d8057a328dd0f11e354e5cee5c3f2c0fedb1cbf7", 182 | "https://deno.land/x/mqtt@0.1.2/packets/pingreq.ts": "db5aad46559be466702274ad6bc458c09c2a104d46e5fd25c312a04eb237528f", 183 | "https://deno.land/x/mqtt@0.1.2/packets/pingres.ts": "e5ab665c6c2e430458b1a938bf12aae3989f8606328254546a0d9df011b541fa", 184 | "https://deno.land/x/mqtt@0.1.2/packets/puback.ts": "dea32076f3c4b47b97f62f36adff95544599a6f1d6f9b41439dba1bcb784b3ce", 185 | "https://deno.land/x/mqtt@0.1.2/packets/pubcomp.ts": "1c06dee45ca925b6e36eaf9b39dc5479d09ed50320946a7c56281f997666ac36", 186 | "https://deno.land/x/mqtt@0.1.2/packets/publish.ts": "edd807d1540a602df631ceffe6ae2966c0859785ca2234a9c73c339814f0d0db", 187 | "https://deno.land/x/mqtt@0.1.2/packets/pubrec.ts": "f58a32f907ecb90b5170099bd3d649ac518ebf4fb0a75697ec81fb300faff7cb", 188 | "https://deno.land/x/mqtt@0.1.2/packets/pubrel.ts": "0eb34365c4f055649a426d1856483f3cb6200b3e6098dc1cf5ec0e8562ed526a", 189 | "https://deno.land/x/mqtt@0.1.2/packets/suback.ts": "cc5f87fa94c63500812bc800bb1d1149c58b591b1c15fd969376083d6b41d660", 190 | "https://deno.land/x/mqtt@0.1.2/packets/subscribe.ts": "bd54a77ec57ffa6bea4c6f72a0aac4d1ef6676a4b00d59b3c9a435b416f3d1c3", 191 | "https://deno.land/x/mqtt@0.1.2/packets/unsuback.ts": "0e204da0419e09c0ef0ffa2c3999214982d7587d086de05761b8fe0522443b33", 192 | "https://deno.land/x/mqtt@0.1.2/packets/unsubscribe.ts": "750128c6526633964faa109b549fa02c1ed5bba0306f151710c86b9935b36d1d", 193 | "https://deno.land/x/mqtt@0.1.2/packets/utf8.ts": "72213bc8d85ae315e876a53350a87c66f98069c3529260667c365d8b7542a34a" 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/calendar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DateObservances, 3 | LiturgicalColour, 4 | LiturgicalSeason, 5 | ResolvedObservance, 6 | Temporal, 7 | aca_calendar, 8 | aca_seasons, 9 | getLiturgicalYear, 10 | get_season_for_date, 11 | } from "../src/libs.ts"; 12 | 13 | const getObservancesForDate = ( 14 | placed_events: DateObservances, 15 | date: Temporal.PlainDate 16 | ): ResolvedObservance[] => { 17 | for (const [dt, obvs] of placed_events) { 18 | if (date.equals(dt)) { 19 | return obvs; 20 | } 21 | } 22 | return []; 23 | }; 24 | 25 | const arrayRotate = (arr: ResolvedObservance[], count: number) => { 26 | const len = arr.length; 27 | arr.push(...arr.splice(0, ((-count % len) + len) % len)); 28 | return arr; 29 | }; 30 | 31 | export const getTodayColour = ( 32 | season: LiturgicalSeason | null, 33 | observances: ResolvedObservance[] 34 | ): [string, LiturgicalColour] => { 35 | // we can be validly outside the pattern of seasons, but there'll be an observance to cover 36 | // us in this case. 37 | const seasonColour = season ? season.colour : LiturgicalColour.COLOUR_GREEN; 38 | const seasonDescription = season ? season.name : ""; 39 | if (observances.length === 0) { 40 | return [seasonDescription, seasonColour]; 41 | } 42 | 43 | // observances without an explicit colour are simply those 44 | // who do not differ from their season 45 | const observanceColour = observances[0].colour || seasonColour; 46 | return [observances[0].slug, observanceColour]; 47 | }; 48 | 49 | export const getCalendarColour = (date: Temporal.PlainDate) => { 50 | const year = getLiturgicalYear(date); 51 | 52 | const calendar = aca_calendar(year); 53 | const seasons = aca_seasons(year); 54 | 55 | // we take the colour from the observances on a modulo N(observances) pattern relative to the year 56 | // this project launched (2024), and in so doing we'll mark the colour of every observance over an 57 | // N-year cycle. 58 | const observances = getObservancesForDate(calendar.observances, date); 59 | arrayRotate(observances, (year - 2024) % observances.length); 60 | 61 | const season = get_season_for_date(seasons, date); 62 | return getTodayColour(season, observances); 63 | }; 64 | -------------------------------------------------------------------------------- /src/colours.ts: -------------------------------------------------------------------------------- 1 | import { LiturgicalColour } from "./libs.ts"; 2 | 3 | export const getHexColour = (colour: LiturgicalColour): string => { 4 | switch (colour) { 5 | case LiturgicalColour.COLOUR_RED: 6 | return "d80707"; 7 | case LiturgicalColour.COLOUR_WHITE: 8 | return "FFD700"; 9 | case LiturgicalColour.COLOUR_GREEN: 10 | return "186420"; 11 | case LiturgicalColour.COLOUR_VIOLET: 12 | return "4B0082"; 13 | case LiturgicalColour.COLOUR_VIOLET_OR_BLUE: 14 | return "0000F9"; 15 | case LiturgicalColour.COLOUR_BLACK: 16 | return "ffffff"; 17 | case LiturgicalColour.COLOUR_ROSE: 18 | return "f485ba"; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/libs.ts: -------------------------------------------------------------------------------- 1 | export { Temporal } from "npm:@js-temporal/polyfill"; 2 | export { Client as MQTTClient } from "https://deno.land/x/mqtt@0.1.2/deno/mod.ts"; 3 | export { 4 | aca_seasons, 5 | calendar as aca_calendar, 6 | } from "https://deno.land/x/churchcalendar@1.5.4/western/calendars/anglican-church-of-australia.ts"; 7 | export { LiturgicalColour } from "https://deno.land/x/churchcalendar@1.5.4/calendar.ts"; 8 | export type { 9 | LiturgicalSeason, 10 | DateObservances, 11 | ResolvedObservance, 12 | } from "https://deno.land/x/churchcalendar@1.5.4/calendar.ts"; 13 | export { getLiturgicalYear } from "https://deno.land/x/churchcalendar@1.5.4/western/liturgicalyear.ts"; 14 | export { get_season_for_date } from "https://deno.land/x/churchcalendar@1.5.4/western/seasons/index.ts"; 15 | export { getSunrise } from "npm:sunrise-sunset-js"; 16 | -------------------------------------------------------------------------------- /src/light.ts: -------------------------------------------------------------------------------- 1 | import subscribe from "https://deno.land/x/mqtt@0.1.2/packets/subscribe.ts"; 2 | import { getHexColour } from "./colours.ts"; 3 | import { LiturgicalColour, MQTTClient } from "./libs.ts"; 4 | 5 | const hex2rgb = (hex: string) => { 6 | const r = parseInt(hex.substring(0, 2), 16); 7 | const g = parseInt(hex.substring(2, 4), 16); 8 | const b = parseInt(hex.substring(4, 6), 16); 9 | return { r, g, b }; 10 | }; 11 | 12 | export const setLight = async ( 13 | url: string, 14 | topic: string, 15 | brightness: number, 16 | colour: LiturgicalColour 17 | ) => { 18 | const client = new MQTTClient({ url }); 19 | await client.connect(); 20 | const payload = JSON.stringify({ 21 | state: brightness === 0 ? "OFF" : "ON", 22 | brightness: brightness, 23 | color: hex2rgb(getHexColour(colour)), 24 | }); 25 | await client.publish(topic, payload); 26 | await client.disconnect(); 27 | }; 28 | 29 | export const watchLights = async ( 30 | url: string, 31 | lights: string[], 32 | callback: (light: string, availability: boolean) => Promise 33 | ) => { 34 | const client = new MQTTClient({ url }); 35 | await client.connect(); 36 | for (const light of lights) { 37 | const topic = `zigbee2mqtt/${light}/availability`; 38 | console.log(await client.subscribe(topic)); 39 | } 40 | const utf8Decoder = new TextDecoder("utf-8"); 41 | client.on("message", (topic: string, message: Uint8Array) => { 42 | callback(topic.split("/")[1], utf8Decoder.decode(message) === "online"); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/pharus.ts: -------------------------------------------------------------------------------- 1 | import { getCalendarColour } from "./calendar.ts"; 2 | import { Temporal } from "./libs.ts"; 3 | import { setLight } from "./light.ts"; 4 | import { ChristHasDied } from "./vigil.ts"; 5 | 6 | export interface Config { 7 | readonly url: string; 8 | readonly lights: string[]; 9 | readonly latitude: number; 10 | readonly longitude: number; 11 | } 12 | 13 | interface InvalidContext { 14 | valid: false; 15 | } 16 | 17 | interface ValidContext { 18 | valid: true; 19 | brightness: number; 20 | colour: string; 21 | slug: string; 22 | wasChanged: boolean; 23 | } 24 | 25 | export type Context = Readonly | Readonly; 26 | 27 | export const pharusApply = async ( 28 | config: Config, 29 | context: Context, 30 | now: Temporal.PlainDateTime 31 | ): Promise => { 32 | const today = now.toPlainDate(); 33 | const [slug, colour] = getCalendarColour(today); 34 | const is_vigil = ChristHasDied( 35 | slug, 36 | now, 37 | today, 38 | config.latitude, 39 | config.longitude 40 | ); 41 | const brightness = is_vigil ? 0 : 255; 42 | let wasChanged = false; 43 | 44 | if ( 45 | !context.valid || 46 | context.brightness !== brightness || 47 | context.colour !== colour 48 | ) { 49 | for (const light of config.lights) { 50 | await setLight( 51 | config.url, 52 | `zigbee2mqtt/${light}/set`, 53 | brightness, 54 | colour 55 | ); 56 | } 57 | wasChanged = true; 58 | } 59 | return { valid: true, brightness, colour, slug, wasChanged }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "./pharus.ts"; 2 | 3 | export interface State { 4 | context: Context; 5 | } 6 | -------------------------------------------------------------------------------- /src/sunrise.ts: -------------------------------------------------------------------------------- 1 | import { Temporal, getSunrise } from "./libs.ts"; 2 | 3 | export const calculateSunrise = ( 4 | today: Temporal.PlainDate, 5 | latitude: number, 6 | longitude: number 7 | ): Temporal.PlainDateTime => { 8 | const sunrise = getSunrise( 9 | latitude, 10 | longitude, 11 | new Date(today.year, today.month - 1, today.day) 12 | ); 13 | return new Temporal.PlainDateTime( 14 | today.year, 15 | today.month, 16 | today.day, 17 | sunrise.getHours(), 18 | sunrise.getMinutes(), 19 | sunrise.getSeconds() 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/today.ts: -------------------------------------------------------------------------------- 1 | // node has a bug that breaks getting a new Temporal date without an 2 | 3 | import { Temporal } from "./libs.ts"; 4 | 5 | // explicitly named timezone 6 | export const get_now = (now: Date): Temporal.PlainDateTime => { 7 | return new Temporal.PlainDateTime( 8 | now.getFullYear(), 9 | now.getMonth() + 1, 10 | now.getDate(), 11 | now.getHours(), 12 | now.getMinutes(), 13 | now.getSeconds() 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/vigil.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "./libs.ts"; 2 | import { calculateSunrise } from "./sunrise.ts"; 3 | 4 | // a solemn liturgical touch: we turn the lamp off from 3pm on Good Friday, 5 | // and back on at the Easter Vigil, sunrise on Easter Sunday. 6 | export const ChristHasDied = ( 7 | slug: string, 8 | now: Temporal.PlainDateTime, 9 | today: Temporal.PlainDate, 10 | latitude: number, 11 | longitude: number 12 | ) => { 13 | const ChristHasRisen = () => { 14 | const sunrise = calculateSunrise(today, latitude, longitude); 15 | if (Temporal.PlainDateTime.compare(sunrise, now) <= 0) { 16 | return true; 17 | } 18 | return false; 19 | }; 20 | 21 | if (slug === "good-friday" && now.hour >= 15) { 22 | return true; 23 | } 24 | if (slug === "holy-saturday") { 25 | return true; 26 | } 27 | if (slug === "easter-day") { 28 | return !ChristHasRisen(); 29 | } 30 | }; 31 | --------------------------------------------------------------------------------