Skip to content

Instantly share code, notes, and snippets.

@jacobator
Created June 11, 2024 10:08
Show Gist options
  • Select an option

  • Save jacobator/0fc3a77628279fb1b400490b17c308f0 to your computer and use it in GitHub Desktop.

Select an option

Save jacobator/0fc3a77628279fb1b400490b17c308f0 to your computer and use it in GitHub Desktop.
Power outage for scriptable Lviv
{
"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