Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
nestorsokil committed Sep 6, 2024
1 parent 3e1ef31 commit f75291b
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 150 deletions.
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# flyctl launch added from .gitignore
**/.env
**/.vscode
**/.telethon
**/__pycache__
**/venv
fly.toml
18 changes: 18 additions & 0 deletions .github/workflows/fly-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/

name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# LightsOff Scheduler
Imports https://poweron.loe.lviv.ua/ into Google Calendar USING AI(omg-omg🔥)

### RUN
### Run

```
cp .env.template .env # update .env with values
docker-compose --build run lightsoff-scheduler
```
OR
```
python -m pip install -r requirements.txt
export $(cat .env | xargs) && python telethon_app.py
```

### Execution sample
![image](https://github.com/user-attachments/assets/526658df-353b-4495-b702-16a446e56521)
Expand Down
16 changes: 16 additions & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# fly.toml app configuration file generated for lightsoff-scheduler on 2024-09-06T09:35:39+03:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'lightsoff-scheduler'
primary_region = 'waw'

[build]

[[vm]]
size = 'shared-cpu-1x'

[[env]]
TELEGRAM_CHANNEL = "@lvivoblenergo"
LOG_LEVEL = "INFO"
11 changes: 7 additions & 4 deletions gcal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

from datetime import datetime, time
from datetime import datetime, time, timedelta
import os
import logging
from googleapiclient.discovery import build
Expand All @@ -12,17 +12,19 @@
credentials = service_account.Credentials.from_service_account_file(GOOGLE_API_KEY_NAME, scopes=SCOPES)
service = build('calendar', 'v3', credentials=credentials)

def to_str(_datetime):
return _datetime.strftime("%Y-%m-%dT%H:%M:%S")

def insert_outage(calendar_id, begin, end, summary="Power Outage", description="Scheduled power outage"):
event = {
"summary": summary,
"description": description,
"start": {
"dateTime": begin,
"dateTime": to_str(begin),
"timeZone": "Europe/Kiev"
},
"end": {
"dateTime": end,
"dateTime": to_str(end),
"timeZone": "Europe/Kiev"
},
"reminders": {
Expand All @@ -41,14 +43,15 @@ def insert_outage(calendar_id, begin, end, summary="Power Outage", description="
def clear_events_for_day(calendar_id, date):
"""Clear all events for a specific day."""
logging.info(f'Clearing day {date} for calendar {calendar_id}')
start_time = datetime.combine(date, time.min).isoformat() + 'Z'
start_time = (datetime.combine(date, time.min) - timedelta(seconds=1)).isoformat() + 'Z'
end_time = datetime.combine(date, time.max).isoformat() + 'Z'

events_result = service.events().list(
calendarId=calendar_id, timeMin=start_time, timeMax=end_time, singleEvents=True, orderBy='startTime').execute()

events = events_result.get('items', [])

print(f"Found {len(events)} events for this day.")
if not events:
logging.info('No events found for this day.')
else:
Expand Down
34 changes: 0 additions & 34 deletions mail.py

This file was deleted.

127 changes: 28 additions & 99 deletions ocr.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import anthropic
import logging
import json
from datetime import datetime
from datetime import datetime, timedelta
import csv
import io


# TODO maybe will move this to Google Calendar API instead of emailing .ics file
def ocr_image_to_calendar_api(image_b64, group="3.2"):
def ocr_image_to_calendar_api_multi(image_b64):
text = _ocr_image(
image_b64, f"I've uploaded an image of a power outage schedule table. Please analyze it and create Google Calendar API request to update calendar for the power outages for group {group}. The image shows hours of the day across the top and group numbers in the first column. Orange cells indicate 'no energy' periods. The date is highlighted at the top. Combine consecutive outage periods into single events. Add a 10-minute notification before each event. Only respond with Google API json requests as an array add a separator '->' and date in format '%Y-%m-%d'.")
parts = text.split("->")
return parts[0], parts[1]


def ocr_image_to_calendar_api_multi(image_b64, group="3.2"):
text = _ocr_image(
image_b64, f"Analyze image of a power outage schedule table which shows hours of the day across the top and group numbers in 1st column. Orange cells indicate 'no energy' periods. The date is highlighted at the top. Combine consecutive outage periods into single events. Only respond with a minified json object key - group number, value - array of pairs [start datetime, end], then add a separator '->' and date in format '%Y-%m-%d'.")
image_b64, 'I uploaded an image of a power outage schedule table which shows hours of the day across the top and group numbers in 1st column. The image may be split into 2 12-hour sub-tables. Orange cells indicate outage periods. The date is highlighted at the top. Combine consecutive outage periods into single events. Only respond with a valid minified json of format {"group": [[full datetime start, full datetime end],[full datetime start, full datetime end]]}, then add a separator "->" and date in format "%Y-%m-%d".')
logging.debug("Received text from OCR: %s", text)
parts = text.split("->")
return json.loads(parts[0].strip()), datetime.strptime(parts[1].strip(), "%Y-%m-%d")


def ocr_image_to_timeframes(image_b64):
text = _ocr_image(
image_b64, 'Don\'t interpret the image text. Turn the large green-orange table into CSV, use 0 for orange, 1 for green, add headers, no prompt, add separator "FOR_DATE:" and date %Y-%m-%d from the top of the image.')
logging.info("Received text from OCR: %s", text)
parts = text.split("FOR_DATE:")
day = datetime.strptime(parts[1].strip(), "%Y-%m-%d")
csv_string = parts[0].strip()
csv_reader = csv.reader(io.StringIO(csv_string))

def ocr_image_to_ics(image_b64, group="3.2"):
return _ocr_image(image_b64, f"I've uploaded an image of a power outage schedule table. Please analyze it and create an .ics file for the power outages for group {group}. The image shows hours of the day across the top and group numbers in the first column. Orange cells indicate 'no energy' periods. Combine consecutive outage periods into single events. Add a 10-minute notification before each event. Provide the .ics file content in a format I can easily copy and save. The date for these events is shown at the top of the image - please use that date when creating the events. Only send the .ics file content, no additional text.")
result = {}
next(csv_reader, None) # skip the headers
for row in csv_reader:
group = row[0]
result[group] = []
for i, cell in enumerate(row[1:]):
if cell == "0":
start = day + timedelta(hours=i)
end = day + timedelta(hours=i + 1)
if i != 0 and len(result[group]) > 0 and result[group][-1] is not None and result[group][-1][1] == start:
result[group][-1][1] = end
else:
result[group].append([start, end])
return result, day


def mock_ocr_to_calendar_api_multi():
Expand All @@ -30,92 +45,6 @@ def mock_ocr_to_calendar_api_multi():
return json.loads(parts[0].strip()), datetime.strptime(parts[1].strip(), "%Y-%m-%d")


def mock_ocr_to_calendar_api():
text = """
[
{
"summary": "Power Outage",
"description": "Scheduled power outage for group 3.2",
"start": {
"dateTime": "2024-09-05T03:00:00+03:00",
"timeZone": "Europe/Kiev"
},
"end": {
"dateTime": "2024-09-05T05:00:00+03:00",
"timeZone": "Europe/Kiev"
},
"reminders": {
"useDefault": false,
"overrides": [
{
"method": "popup",
"minutes": 10
}
]
}
},
{
"summary": "Power Outage",
"description": "Scheduled power outage for group 3.2",
"start": {
"dateTime": "2024-09-05T15:00:00+03:00",
"timeZone": "Europe/Kiev"
},
"end": {
"dateTime": "2024-09-05T17:00:00+03:00",
"timeZone": "Europe/Kiev"
},
"reminders": {
"useDefault": false,
"overrides": [
{
"method": "popup",
"minutes": 10
}
]
}
}
]
->2024-09-05
"""
parts = text.split("->")
return parts[0].strip(), parts[1].strip()


def mock_ocr_to_ics():
return """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Your Organization//EN
BEGIN:VEVENT
SUMMARY:Power Outage (Group 3.2)
DTSTART:20240905T030000
DTEND:20240905T050000
DTSTAMP:20240905T000000Z
UID:[email protected]
DESCRIPTION:Scheduled power outage for Group 3.2
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Power outage starting in 10 minutes
TRIGGER:-PT10M
END:VALARM
END:VEVENT
BEGIN:VEVENT
SUMMARY:Power Outage (Group 3.2)
DTSTART:20240905T170000
DTEND:20240905T190000
DTSTAMP:20240905T000000Z
UID:[email protected]
DESCRIPTION:Scheduled power outage for Group 3.2
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Power outage starting in 10 minutes
TRIGGER:-PT10M
END:VALARM
END:VEVENT
END:VCALENDAR
"""


def _ocr_image(image_b64, prompt):
client = anthropic.Anthropic() # defaults to os.environ.get("ANTHROPIC_API_KEY)
logging.info("Sending image to Anthropics API for OCR...")
Expand Down
41 changes: 29 additions & 12 deletions telethon_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,36 @@
import json

from telethon import TelegramClient, events
from telethon.sessions import StringSession

api_id = os.environ.get("TELEGRAM_API_ID")
api_hash = os.environ.get("TELEGRAM_API_HASH")
phone_number = os.environ.get("TELEGRAM_PHONE_NUMBER")
channel_username = os.environ.get("TELEGRAM_CHANNEL", '@lvivoblenergo')
session_string = os.environ.get("TELEGRAM_SESSION_STRING")

subscribers = os.environ.get("EMAIL_SUBSCRIBERS").split(',')
calendars = json.loads(os.environ.get("CALENDARS_MAPPING_JSON"))

client = TelegramClient('.telethon/session_name', api_id, api_hash)
session = StringSession(session_string) if session_string else StringSession()
client = TelegramClient(session, api_id, api_hash)

calendars = json.loads(os.environ.get("CALENDARS_MAPPING_JSON"))

def clean_downloads():
import os
import shutil
folder = '.telethon/downloads'
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
except Exception as e:
logging.error('Failed to delete %s. Reason: %s' % (file_path, e))

def to_str(_datetime):
return _datetime.strftime("%Y-%m-%dT%H:%M:%S")

@client.on(events.NewMessage(chats=channel_username))
async def handler(event):
Expand All @@ -29,15 +48,11 @@ async def handler(event):
with open(photo_path, "rb") as file:
file_data = file.read()
image_base64 = base64.b64encode(file_data).decode("ascii")
# ics = ocr.ocr_image_to_ics(image_base64)
# ics = ocr.mock_ocr_to_ics()
# mail.send_email_with_invite(ics, subscribers)

# events, day = ocr.mock_ocr_to_calendar_api()
# events, day = ocr.ocr_image_to_calendar_api(image_base64)
# events, day = ocr.mock_ocr_to_calendar_api_multi()

events, day = ocr.ocr_image_to_calendar_api_multi(image_base64)
#events, day = ocr.ocr_image_to_calendar_api_multi(image_base64)
events, day = ocr.ocr_image_to_timeframes(image_base64)
logging.debug(f"OCR results: {events}, {day}")

for group, timeframes in events.items():
calendar_id = calendars[group]
Expand All @@ -50,12 +65,14 @@ async def handler(event):
gcal.insert_outage(calendar_id=calendar_id, begin=begin, end=end,
summary=f"Power Outage ({group})", description=f"Scheduled power outage for {group}")
logging.info("Updated schedules in Google Calendar")
# todo delete file

logging.debug("Cleaning downloads folder...")
clean_downloads()
logging.debug("Done cleaning downloads folder...")

def main():
logging.info("Starting client...")
client.start(phone_number)
logging.debug("Session string: %s", client.session.save())
client.run_until_disconnected()


Expand Down

0 comments on commit f75291b

Please sign in to comment.