Created
June 11, 2024 10:08
-
-
Save jacobator/0fc3a77628279fb1b400490b17c308f0 to your computer and use it in GitHub Desktop.
Power outage for scriptable Lviv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "always_run_in_app" : false, | |
| "icon" : { | |
| "color" : "deep-blue", | |
| "glyph" : "lightbulb" | |
| }, | |
| "name" : "Power outage 2.1", | |
| "script" : "\/\/ colors\nconst DEFAULT_TEXT_COLOR = Color.dynamic(Color.black(), Color.white());\nconst SECONDARY_TEXT_COLOR = new Color('#bababa');\nconst OUTAGE_COLOR = Color.red();\nconst NO_OUTAGE_COLOR = Color.green();\nconst MAYBE_OUTAGE_COLOR = Color.orange();\nconst WIDGET_COLOR = Color.dynamic(Color.white(), new Color('#1c1c1e'));\n\n\/\/ text\nconst DEFAULT_FONT = Font.mediumSystemFont(14);\nconst LARGE_FONT = Font.lightSystemFont(37);\nconst SECONDARY_FONT = Font.boldSystemFont(14);\nconst OUTAGE_MESSAGE = 'Outage';\nconst NO_OUTAGE_MESSAGE = 'Okay';\nconst MAYBE_OUTAGE_MESSAGE = 'Maybe';\n\n\/\/ other\nconst OUTAGE_CODE = '-';\nconst NO_OUTAGE_CODE = '+';\nconst MAYBE_OUTAGE_CODE = '0';\nconst SEEDS = new Map([\n [\n '1.1',\n '0-++++0-++++-0++++-0++++++++-0++++-0++-0++++-0++0-++++0-++++++++0-++++0-++0-++++0-++',\n ],\n [\n '1.2',\n '-0++++-0++++0-++++0-++++++++0-++++0-++0-++++0-++-0++++-0++++++++-0++++-0++-0++++-0++',\n ],\n [\n '2.1',\n '++++0-++++0-++++-0++++-0++-0++++-0++-0++++-0++++++++0-++++0-++0-++++0-++0-++++0-++++',\n ],\n [\n '2.2',\n '++++-0++++-0++++0-++++0-++0-++++0-++0-++++0-++++++++-0++++-0++-0++++-0++-0++++-0++++',\n ],\n [\n '3.1',\n '++0-++++0-++++-0++++-0++-0++++-0++++++++-0++++-0++0-++++0-++0-++++0-++++++++0-++++0-',\n ],\n [\n '3.2',\n '++-0++++-0++++0-++++0-++0-++++0-++++++++0-++++0-++-0++++-0++-0++++-0++++++++-0++++-0',\n ],\n]);\nconst HOUR_MS = 60 * 60 * 1000;\nconst HOUR_BLOCK_SIZE = 2;\nconst MIN_TIMELINE_HOURS_LABEL = 4;\nconst WIDGET_PADDING = 15;\n\nfunction getGroup() {\n const DEFAULT_GROUP = '1.1';\n let group = args.widgetParameter;\n if (!SEEDS.has(group)) {\n group = DEFAULT_GROUP;\n }\n return group;\n}\n\nfunction getStatusIndex(date) {\n const day = date.getDay();\n const hours = date.getHours();\n return day * 24 + hours;\n}\n\nfunction getStatus(pattern, date) {\n const status = pattern[getStatusIndex(date)];\n return status;\n}\n\nfunction offsetDate(date, hours) {\n const newDate = new Date(date.getTime() + hours * HOUR_MS);\n return newDate;\n}\n\nfunction getStartDate(date) {\n const endDate = getEndDate(date);\n const startDate = offsetDate(endDate, -HOUR_BLOCK_SIZE);\n return startDate;\n}\n\nfunction getEndDate(date) {\n let offset = (date.getHours() % HOUR_BLOCK_SIZE) - 1;\n if (offset < 0) {\n offset += HOUR_BLOCK_SIZE;\n }\n offset = HOUR_BLOCK_SIZE - offset;\n let endDate = offsetDate(date, offset);\n endDate.setMinutes(0);\n endDate.setSeconds(0);\n endDate.setMilliseconds(0);\n return endDate;\n}\n\nfunction getStatusString(status) {\n switch (status) {\n case OUTAGE_CODE:\n return OUTAGE_MESSAGE;\n case NO_OUTAGE_CODE:\n return NO_OUTAGE_MESSAGE;\n case MAYBE_OUTAGE_CODE:\n default:\n return MAYBE_OUTAGE_MESSAGE;\n }\n}\n\nfunction getStatusColor(status) {\n switch (status) {\n case OUTAGE_CODE:\n return OUTAGE_COLOR;\n case NO_OUTAGE_CODE:\n return NO_OUTAGE_COLOR;\n case MAYBE_OUTAGE_CODE:\n default:\n return MAYBE_OUTAGE_COLOR;\n }\n}\n\nfunction addText(widget, text, params = {}) {\n const font = params.font || DEFAULT_FONT;\n const textColor = params.color || DEFAULT_TEXT_COLOR;\n const widgetText = widget.addText(text);\n widgetText.font = font;\n widgetText.textColor = textColor;\n return widgetText;\n}\n\nasync function setNotifications(date) {\n const BEFORE_EVENT_MINUTES = 30;\n const BEFORE_EVENT_HOURS = BEFORE_EVENT_MINUTES \/ 60;\n const untilEventMs = date.getTime() - CURRENT_DATE.getTime();\n\n if (untilEventMs < BEFORE_EVENT_HOURS * HOUR_MS) {\n return;\n }\n\n const identifier = `${CURRENT_GROUP}-outage`;\n const status = getStatus(CURRENT_PATTERN, date);\n const title = `Lviv energy schedule: group ${CURRENT_GROUP}`;\n let text = '';\n switch (status) {\n case OUTAGE_CODE:\n text = `Power outage in ${BEFORE_EVENT_MINUTES} minutes`;\n break;\n case MAYBE_OUTAGE_CODE:\n text = `Possible power outage in ${BEFORE_EVENT_MINUTES} minutes`;\n break;\n }\n\n if (!text) {\n return;\n }\n\n const notifications = await Notification.allPending();\n const notification = notifications.find(n => n.identifier === identifier);\n const triggerDate = offsetDate(date, -BEFORE_EVENT_HOURS);\n\n if (!notification) {\n const notification = new Notification();\n notification.identifier = identifier;\n notification.title = title;\n notification.body = text;\n notification.setTriggerDate(triggerDate);\n notification.schedule();\n }\n}\n\nfunction getPattern(seed) {\n const full = seed\n .split('')\n .map(s => s.repeat(HOUR_BLOCK_SIZE))\n .join('');\n const pattern = `${full.slice(-1)}${full.slice(0, -1)}`;\n return pattern;\n}\n\nfunction getMediumWidgetSize() {\n const screenSize = Device.screenSize();\n const height = Math.max(screenSize.width, screenSize.height);\n const sizeMap = new Map([\n [932, new Size(364, 170)],\n [926, new Size(364, 170)],\n [896, new Size(360, 169)],\n [736, new Size(348, 157)],\n [852, new Size(338, 158)],\n [844, new Size(338, 158)],\n [812, new Size(329, 155)],\n [667, new Size(321, 148)],\n [780, new Size(329, 155)],\n [568, new Size(292, 141)],\n ]);\n \/\/ iPhone 13\n const defaultSize = sizeMap.get(844);\n switch (true) {\n case Device.isPhone():\n return sizeMap.get(height) || defaultSize;\n case Device.isPad():\n default:\n return defaultSize;\n }\n}\n\nfunction setupRootWidget() {\n const widget = new ListWidget();\n widget.backgroundColor = WIDGET_COLOR;\n return widget;\n}\n\nfunction drawStatus(widget) {\n addText(widget, `Group: ${CURRENT_GROUP}`);\n addText(widget, `${getStatusString(CURRENT_STATUS)}`, {\n font: LARGE_FONT,\n color: getStatusColor(CURRENT_STATUS),\n });\n widget.addSpacer();\n}\n\nfunction drawUpNext(widget, alighRight = false) {\n let line1 = widget;\n let line2 = widget;\n const formatter = new DateFormatter();\n formatter.dateFormat = 'HH:mm';\n\n if (alighRight) {\n line1 = widget.addStack();\n line1.addSpacer();\n line2 = widget.addStack();\n line2.addSpacer();\n widget.addSpacer();\n }\n\n if (CURRENT_STATUS === OUTAGE_CODE) {\n addText(line1, `Outage ends:`);\n addText(line2, ` ${formatter.string(getEndDate(CURRENT_DATE))}`);\n } else {\n const nextOutageStartDate = getNextEventStartDate(OUTAGE_CODE);\n addText(line1, `Next outage:`);\n addText(\n line2,\n `${formatter.string(nextOutageStartDate)} - ${formatter.string(\n getEndDate(nextOutageStartDate),\n )}`,\n );\n }\n}\n\nfunction drawTimeline(widget) {\n const OFFSET_HOURS = -3;\n const TIMELINE_HOURS_COUNT = 25;\n const startDate = getStartDate(CURRENT_DATE);\n const graphStartDate = offsetDate(startDate, OFFSET_HOURS);\n\n const canvas = new DrawContext();\n canvas.size = widget.size;\n canvas.opaque = false;\n canvas.respectScreenScale = true;\n canvas.setFont(SECONDARY_FONT);\n canvas.setTextColor(SECONDARY_TEXT_COLOR);\n canvas.setTextAlignedCenter();\n\n const TEXT_HEIGHT = 20;\n const HOUR_LINE_HEIGHT = 4;\n const PIN_RADIUS = 3;\n const CROP_HOUR_SIZE = 1;\n const OFFSET_TOP = widget.size.height - TEXT_HEIGHT - HOUR_LINE_HEIGHT;\n const startIndex = getStatusIndex(graphStartDate);\n const hourWidth = widget.size.width \/ TIMELINE_HOURS_COUNT;\n const formatter = new DateFormatter();\n formatter.dateFormat = 'HH';\n\n \/\/ draw timeline\n let prevStatus =\n CURRENT_PATTERN[\n (startIndex + CURRENT_PATTERN.length - 1) % CURRENT_PATTERN.length\n ];\n for (let i = 0; i < TIMELINE_HOURS_COUNT; i++) {\n const status = CURRENT_PATTERN[(startIndex + i) % CURRENT_PATTERN.length];\n canvas.setFillColor(getStatusColor(status));\n canvas.fillRect(\n new Rect(\n i * hourWidth + CROP_HOUR_SIZE,\n OFFSET_TOP,\n hourWidth - 2 * CROP_HOUR_SIZE,\n HOUR_LINE_HEIGHT,\n ),\n );\n if ((startIndex + i + 1) % 24 === 0) {\n canvas.fillRect(\n new Rect(\n (i + 1) * hourWidth - CROP_HOUR_SIZE,\n OFFSET_TOP - 2,\n 2 * CROP_HOUR_SIZE,\n HOUR_LINE_HEIGHT + 4,\n ),\n );\n }\n\n if (\n (i > 0 && status !== prevStatus) ||\n (startIndex + i - 1) % MIN_TIMELINE_HOURS_LABEL === 0\n ) {\n canvas.drawTextInRect(\n formatter.string(offsetDate(graphStartDate, i)),\n new Rect(\n (i - HOUR_BLOCK_SIZE \/ 2) * hourWidth,\n OFFSET_TOP + HOUR_LINE_HEIGHT,\n hourWidth * HOUR_BLOCK_SIZE,\n TEXT_HEIGHT,\n ),\n );\n\n prevTimeLabelIndex = i;\n }\n\n prevStatus = status;\n }\n\n \/\/ draw current time\n const currentTimeX =\n (hourWidth * (CURRENT_DATE.getTime() - graphStartDate.getTime())) \/ HOUR_MS;\n const height = OFFSET_TOP - PIN_RADIUS;\n const path = new Path();\n path.move(new Point(currentTimeX, OFFSET_TOP));\n path.addLine(new Point(currentTimeX, OFFSET_TOP - height));\n canvas.addPath(path);\n canvas.setStrokeColor(getStatusColor(CURRENT_STATUS));\n canvas.strokePath();\n canvas.setFillColor(getStatusColor(CURRENT_STATUS));\n canvas.fillEllipse(\n new Rect(\n currentTimeX - PIN_RADIUS,\n OFFSET_TOP - height - PIN_RADIUS,\n PIN_RADIUS * 2,\n PIN_RADIUS * 2,\n ),\n );\n\n widget.addImage(canvas.getImage());\n}\n\nfunction drawSmallWidget() {\n drawStatus(rootWidget);\n drawUpNext(rootWidget);\n\n rootWidget.presentSmall();\n}\n\nfunction drawMediumWidget() {\n const widgetSize = getMediumWidgetSize();\n const MEDIUM_WIDGET_WIDTH = widgetSize.width - WIDGET_PADDING * 2;\n const MEDIUM_WIDGET_HEIGHT = widgetSize.height - WIDGET_PADDING * 2;\n const TOP_PORTION_HEIGHT = 0.6 * MEDIUM_WIDGET_HEIGHT;\n const BOTTOM_PORTION_HEIGHT = MEDIUM_WIDGET_HEIGHT - TOP_PORTION_HEIGHT;\n\n const topWidget = rootWidget.addStack();\n topWidget.size = new Size(MEDIUM_WIDGET_WIDTH, TOP_PORTION_HEIGHT);\n\n const leftWidget = topWidget.addStack();\n leftWidget.size = new Size(MEDIUM_WIDGET_WIDTH \/ 2, TOP_PORTION_HEIGHT);\n leftWidget.layoutVertically();\n\n const rightWidget = topWidget.addStack();\n rightWidget.size = new Size(MEDIUM_WIDGET_WIDTH \/ 2, TOP_PORTION_HEIGHT);\n rightWidget.layoutVertically();\n\n const bottomWidget = rootWidget.addStack();\n bottomWidget.size = new Size(MEDIUM_WIDGET_WIDTH, BOTTOM_PORTION_HEIGHT);\n\n drawStatus(leftWidget);\n drawUpNext(rightWidget, true);\n drawTimeline(bottomWidget);\n\n rootWidget.presentMedium();\n}\n\nfunction drawWidget() {\n switch (config.widgetFamily) {\n case 'small':\n drawSmallWidget();\n break;\n case 'medium':\n case 'large':\n case 'extraLarge':\n default:\n drawMediumWidget();\n break;\n }\n}\n\nfunction getNextEventStartDate(status) {\n let nextEventStartDate = getEndDate(CURRENT_DATE);\n while (status && getStatus(CURRENT_PATTERN, nextEventStartDate) !== status) {\n nextEventStartDate = getEndDate(nextEventStartDate);\n }\n return nextEventStartDate;\n}\n\nconst CURRENT_GROUP = getGroup();\nconst CURRENT_DATE = new Date();\nconst CURRENT_PATTERN = getPattern(SEEDS.get(CURRENT_GROUP));\nconst CURRENT_STATUS = getStatus(CURRENT_PATTERN, CURRENT_DATE);\n\n\/\/ setNotifications(getNextEventStartDate());\n\nconst rootWidget = setupRootWidget();\nrootWidget.url = 'https:\/\/poweron.loe.lviv.ua';\ndrawWidget();\n\nScript.setWidget(rootWidget);\nScript.complete();\n", | |
| "share_sheet_inputs" : [ | |
| ] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment