timex_datalink_caldav 0.3.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d31450cb602c3c36c121b4ffd2240664ed8f9e550388a246f1c5e169d12f7fa
4
- data.tar.gz: 351b4213184ca2652205c80e99b7997538385bd0fab7e6ee3f4a6a57f1977247
3
+ metadata.gz: 8301c0dee4e085d6ebbbebbcd41872211625f6798980f688cf66765550519622
4
+ data.tar.gz: 30171ce1fcdb85fa57b17121982c85ab5805cdbef1f12106d56561397bd82ee6
5
5
  SHA512:
6
- metadata.gz: '08bf1aa6454647ccc74a568cb0e54e5b9724394da3f0d8ff6a8bcb0205d63ddb35208b40d99cbdbdab0be8c6b6e5c0c6febdb9db3369c415bad5cfdba7be7cb5'
7
- data.tar.gz: c2334ebf8d3d5515f4bce340808b61cacdc39ccdeaad6d2b7e7e356340fb97cf049c0316d1587d37b74dad9f4a593dcf91be66399bbfc3075018ef023a0e267d
6
+ metadata.gz: 31a43cf2e995bcd29d51ebae9d3a30002239ac0bea6862f97b0f967daf25734409432e75fbced7727ecc2126e6ec509bdb0c09fd6dc7fcbffed87e9c124d73a5
7
+ data.tar.gz: 5b8b1f14b62a03c6354c2b555651ae6336b6248ddc633b4749b966929c2e8b6e39c8d6f8845cb57329a46a5dc18c35522bd8a193c115efa781922ead52853705
data/Gemfile CHANGED
@@ -20,3 +20,13 @@ gem "icalendar-recurrence", "~> 1.1"
20
20
  gem "activesupport", "~> 7.0"
21
21
 
22
22
  gem "tzinfo", "~> 2.0"
23
+
24
+ gem "yaml", "~> 0.2.1"
25
+
26
+ gem "text", "~> 1.3"
27
+
28
+ gem "humanize", "~> 2.5"
29
+
30
+ gem "down", "~> 5.4"
31
+
32
+ gem "ruby-progressbar", "~> 1.13"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- timex_datalink_caldav (0.2.0)
4
+ timex_datalink_caldav (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -24,6 +24,8 @@ GEM
24
24
  diff-lcs (1.5.0)
25
25
  domain_name (0.5.20190701)
26
26
  unf (>= 0.0.5, < 1.0.0)
27
+ down (5.4.1)
28
+ addressable (~> 2.8)
27
29
  ffi (1.15.5)
28
30
  ffi-compiler (1.0.1)
29
31
  ffi (>= 1.0.0)
@@ -36,6 +38,7 @@ GEM
36
38
  http-cookie (1.0.5)
37
39
  domain_name (~> 0.5)
38
40
  http-form_data (2.3.0)
41
+ humanize (2.5.1)
39
42
  i18n (1.13.0)
40
43
  concurrent-ruby (~> 1.0)
41
44
  icalendar (2.8.0)
@@ -67,8 +70,10 @@ GEM
67
70
  diff-lcs (>= 1.2.0, < 2.0)
68
71
  rspec-support (~> 3.12.0)
69
72
  rspec-support (3.12.0)
73
+ ruby-progressbar (1.13.0)
70
74
  rubyserial (0.6.0)
71
75
  ffi (~> 1.9, >= 1.9.3)
76
+ text (1.3.1)
72
77
  timex_datalink_client (0.11.0)
73
78
  activemodel (~> 7.0.4)
74
79
  crc (~> 0.4.2)
@@ -79,6 +84,7 @@ GEM
79
84
  unf (0.1.4)
80
85
  unf_ext
81
86
  unf_ext (0.0.8.2)
87
+ yaml (0.2.1)
82
88
 
83
89
  PLATFORMS
84
90
  arm64-darwin-22
@@ -86,13 +92,18 @@ PLATFORMS
86
92
  DEPENDENCIES
87
93
  activesupport (~> 7.0)
88
94
  calendav (~> 0.4.0)
95
+ down (~> 5.4)
96
+ humanize (~> 2.5)
89
97
  icalendar (~> 2.8)
90
98
  icalendar-recurrence (~> 1.1)
91
99
  rake (~> 13.0)
92
100
  rspec (~> 3.12)
101
+ ruby-progressbar (~> 1.13)
102
+ text (~> 1.3)
93
103
  timex_datalink_caldav!
94
104
  timex_datalink_client (~> 0.11.0)
95
105
  tzinfo (~> 2.0)
106
+ yaml (~> 0.2.1)
96
107
 
97
108
  BUNDLED WITH
98
109
  2.4.10
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Willy Hardy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # TimexDatalinkCaldav
2
2
 
3
- TimexDatalinkCaldav is a simple Ruby gem designed to sync events from a CalDAV server to a Timex Datalink watch. It can also be used as a standalone command-line interface (CLI) tool.
3
+ TimexDatalinkCaldav is a simple Ruby gem designed to sync events from a CalDAV server or an ical formatted ics file to a Timex Datalink watch. It can also be used as a standalone command-line interface (CLI) tool.
4
+
5
+ ## Pre-requisites
6
+
7
+ If you need to install Ruby, follow the Ruby installation instructions [here](https://www.ruby-lang.org/en/documentation/installation/).
4
8
 
5
9
  ## Installation
6
10
 
@@ -13,9 +17,7 @@ gem install timex_datalink_caldav
13
17
  Or add this line to your application's Gemfile:
14
18
 
15
19
  ```ruby
16
- source "https://rubygems.pkg.github.com/wjhrdy" do
17
- gem "timex_datalink_caldav"
18
- end
20
+ gem "timex_datalink_caldav"
19
21
  ```
20
22
 
21
23
  And then execute:
@@ -33,26 +35,47 @@ Here's an example of how to use the tool in your Ruby code:
33
35
  ```ruby
34
36
  require 'timex_datalink_caldav'
35
37
 
36
- client = TimexDatalinkCaldav::Client.new(your_username, your_password, your_server_uri, your_device)
37
- client.sync_to_watch
38
+ client = TimexDatalinkCaldav::Client.new(your_username, your_password, your_server_uri, your_device, your_protocol_version, days_forward)
39
+
40
+ client.parse_events
41
+ client.write_to_watch
38
42
  ```
39
43
 
40
44
  ### As a CLI Tool
41
45
 
42
- After installing the gem, you can use it as a CLI tool:
46
+ After installing the gem, you can use it as a CLI tool. You can specify the CalDAV server details directly on the command line:
43
47
 
44
48
  ```sh
45
- timex_datalink_caldav -u https://caldavendpoint.com -n your_username -p your_password -d your_device
49
+ timex_datalink_caldav -u https://caldavendpoint.com -n your_username -p your_password -d your_device -a your_protocol_version -f days_forward
46
50
  ```
47
51
 
48
- Please replace `caldavendpoint.com` `your_username`, `your_password`, and `your_device` with your actual CalDAV server, username, password, and serial device respectively.
52
+ Please replace `https://caldavendpoint.com`, `your_username`, `your_password`, `your_device`, `your_protocol_version`, and `days_forward` with your actual CalDAV server URI, username, password, serial device, protocol version, and number of days to look forward for events, respectively.
53
+
54
+ Or you can provide these details in a configuration file:
55
+
56
+ ```sh
57
+ timex_datalink_caldav -c config.yml -a 1 -d /dev/tty.usbmodem0000000000001 -f 7
58
+ ```
59
+
60
+ The configuration file should be a YAML file in the following format:
61
+
62
+ ```yaml
63
+ endpoints:
64
+ - uri: https://www.google.com/calendar/dav/email@gmail.com/events
65
+ user: email@gmail.com
66
+ password: app_password
67
+ - uri: https://caldavendpoint2.com
68
+ user: your_username2
69
+ password: your_password2
70
+ - uri: https://icalendpoint.com/example.ics
71
+ ```
49
72
 
50
- The device is a serial device that flashes an led when it receives data. On Linux, this is usually `/dev/ttyUSB0`. On macOS, this is usually `/dev/cu.usbserial-0001`. On Windows, this is usually `COM1`.
73
+ The device is a serial device that flashes an led when it receives data. On Linux, this is usually `/dev/tty*`. On macOS, this is usually `$(ls /dev/tty.usbmodem* | head -n 1)`. On Windows, this is usually `COM1`.
51
74
 
52
- If you want to use this I highly recommend pairing it with the Raspberry Pi Pico and [this project](https://github.com/famiclone6502/DIY_Datalink_Adapter). It is the cheapest and easiest way to get a serial device that works with the Timex Datalink watch.
75
+ If you want to use this, I highly recommend pairing it with the Raspberry Pi Pico and [this project](https://github.com/famiclone6502/DIY_Datalink_Adapter). It is the cheapest and easiest way to get a serial device that works with the Timex Datalink watch.
53
76
 
54
- ## Note
77
+ ## Notes
55
78
 
56
- Ensure you have the necessary dependencies installed on your system and you have the correct permissions to access the specified device.
79
+ - This gem is not affiliated with Timex, nor is it affiliated with any CalDAV server. It is simply a tool that I wrote to sync my events from my CalDAV server to my Timex Datalink watch.
57
80
 
58
- The tool currently filters down to events that have attendees and converts event times to Eastern Standard Time (EST). Events are sorted by time before syncing to the watch.
81
+ - This gem uses the anniversary feature for full day events, and the appointments feature for events with a start and end time.
data/bin/get_ebrain_db ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'down'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'ruby-progressbar'
7
+
8
+ # Define URL and file names
9
+ url = "https://archive.org/download/ebrain-1.1.6/ebrain-1.1.6.iso"
10
+ iso_file = File.join(Dir.tmpdir, "ebrain-1.1.6.iso")
11
+ msi_file = File.join(Dir.tmpdir, "eBrain.MSI")
12
+ cab_file = File.join(Dir.tmpdir, "Cabs.w4.cab")
13
+ mdb_file1 = File.join(Dir.tmpdir, "pcvocab.mdb1")
14
+ mdb_file = "pcvocab.mdb" # This is in the current directory
15
+
16
+ # Check if 7zip is installed
17
+ unless system('which 7z > /dev/null 2>&1')
18
+ puts "7zip is not installed. Please download and install 7zip from https://www.7-zip.org/download.html"
19
+ exit 1
20
+ end
21
+
22
+ # Initialize the progress bar
23
+ progressbar = nil
24
+
25
+ # Download the ISO file
26
+ puts "Downloading ISO file..."
27
+ Down.download(url,
28
+ destination: iso_file,
29
+ content_length_proc: ->(content_length) {
30
+ progressbar = ProgressBar.create(
31
+ title: 'Download Progress',
32
+ total: content_length,
33
+ format: '%a %bᗧ%i %p%% %t, Estimated time: %e'
34
+ )
35
+ },
36
+ progress_proc: ->(progress) {
37
+ progressbar.progress = progress
38
+ }
39
+ )
40
+
41
+ # Extract the MSI file from the ISO
42
+ puts "Extracting MSI file..."
43
+ system("7z e #{iso_file} -o#{File.dirname(msi_file)} #{File.basename(msi_file)}")
44
+
45
+ # Extract the CAB file from the MSI
46
+ puts "Extracting CAB file..."
47
+ system("7z e #{msi_file} -o#{File.dirname(cab_file)} #{File.basename(cab_file)}")
48
+
49
+ # Extract the MDB file from the CAB
50
+ puts "Extracting MDB file..."
51
+ system("7z e #{cab_file} -o#{File.dirname(mdb_file1)} #{File.basename(mdb_file1)}")
52
+
53
+ # Rename and move the MDB file to the current directory
54
+ puts "Renaming and moving MDB file..."
55
+ FileUtils.mv mdb_file1, mdb_file
56
+
57
+ puts "Done."
@@ -0,0 +1,7 @@
1
+ # Examples
2
+
3
+ This directory contains sample configurations and code for the timex_datalink_caldav gem.
4
+
5
+ ## Configuration
6
+
7
+ `caldav.yaml.sample` is a sample configuration file for CalDAV endpoints. You should rename it to `caldav.yaml` and adjust the URI, username, and password for your endpoints.
@@ -0,0 +1,8 @@
1
+ endpoints:
2
+ - uri: https://example.com/caldav1
3
+ user: user1
4
+ password: password1
5
+ - uri: https://example.com/caldav2
6
+ user: user2
7
+ password: password2
8
+ - uri: https://example.com/example.ical
@@ -1,25 +1,30 @@
1
1
  require 'calendav'
2
+ require 'icalendar'
2
3
  require 'icalendar/recurrence'
3
4
  require 'timex_datalink_client'
4
5
  require 'active_support/time'
5
6
  require 'tzinfo'
7
+ require 'humanize'
8
+ require_relative 'similar_word'
6
9
 
7
10
  module TimexDatalinkCaldav
8
11
  class Client
9
- def initialize(user, password, server_url, serial_device, protocol_version)
12
+ def initialize(user, password, server_url, serial_device, protocol_version, days_forward = 1)
10
13
  @user = user
11
14
  @password = password
12
15
  @server_url = server_url
13
16
  @serial_device = serial_device
17
+ @days_forward = days_forward
14
18
  @protocol_version = protocol_version.to_i
15
19
  @protocol_class = case @protocol_version
16
- when 1 then TimexDatalinkClient::Protocol1
17
- when 3 then TimexDatalinkClient::Protocol3
18
- when 4 then TimexDatalinkClient::Protocol4
19
- else
20
- raise ArgumentError, "Invalid protocol version: #{@protocol_version}"
21
- end
22
- end
20
+ when 1 then TimexDatalinkClient::Protocol1
21
+ when 3 then TimexDatalinkClient::Protocol3
22
+ when 4 then TimexDatalinkClient::Protocol4
23
+ when 7 then TimexDatalinkClient::Protocol7
24
+ else
25
+ raise ArgumentError, "Invalid protocol version: #{@protocol_version}"
26
+ end
27
+ end
23
28
 
24
29
  def get_localzone
25
30
  TZInfo::Timezone.get(TZInfo::Timezone.all_country_zones.detect {|z| z.period_for_local(Time.now).utc_total_offset == Time.now.utc_offset}.identifier)
@@ -33,49 +38,140 @@ module TimexDatalinkCaldav
33
38
  authentication: :basic_auth
34
39
  )
35
40
 
36
- # Create a new client with the credentials
37
- client = Calendav.client(credentials)
41
+ if @user && @password
42
+ # Create a new client with the credentials
43
+ client = Calendav.client(credentials)
38
44
 
39
- # Get events from the calendar for the next day
40
- client.events.list(@server_url, from: Time.now, to: Time.now + 24*60*60)
45
+ # Get events from the calendar for the next day
46
+ caldav_events = client.events.list(@server_url, from: Time.now, to: Time.now + @days_forward*24*60*60)
47
+ events = caldav_events.map { |event| Icalendar::Event.parse(event.calendar_data).first }
48
+ else
49
+ cal_file = URI.open(@server_url)
50
+ cals = Icalendar::Calendar.parse(cal_file)
51
+ cal = cals.first
52
+ events = cal.events
53
+ end
54
+
55
+ events
41
56
  end
42
57
 
43
- def sync_to_watch
58
+ def parse_events
44
59
  events = get_events
45
-
60
+
46
61
  appointments = []
62
+ anniversaries = []
47
63
  appointment_map = {} # Used to avoid duplicate appointments
48
-
64
+ anniversary_map = {} # Used to avoid duplicate anniversaries
65
+
66
+ phrase_builder = SimlarWord.new(database: "pcvocab.mdb") if @protocol_version == 7
67
+
49
68
  events.each do |event|
50
- ical_events = Icalendar::Event.parse(event.calendar_data)
51
- if ical_events.any?
52
- ical_event = ical_events.first
53
- if ical_event.attendee&.any? # Exclude events without attendees
54
- next_occurrence = ical_event.occurrences_between(Time.now, Time.now + 24*60*60).first
55
- if next_occurrence
56
- puts get_localzone
57
- est_time = next_occurrence.start_time.in_time_zone(get_localzone)
58
- key = "#{est_time}_#{ical_event.summary.to_s}"
59
- unless appointment_map[key] # Check if the event is already in the map
60
- puts "Adding appointment: #{ical_event.summary.to_s} at time #{est_time}"
61
- appointment = @protocol_class::Eeprom::Appointment.new(
62
- time: est_time,
63
- message: ical_event.summary.to_s
64
- )
65
- appointments << appointment
66
- appointment_map[key] = true
67
- end
68
- end
69
+ next unless event && event.dtstart && event.dtend
70
+
71
+ summary_words = parse_summary(event)
72
+
73
+ occurrences = event.occurrences_between(Time.now, Time.now + @days_forward*24*60*60)
74
+
75
+ if @protocol_version == 7
76
+ if all_day_event?(event)
77
+ add_anniversary_event(anniversary_map, anniversaries, phrase_builder, summary_words, occurrences)
78
+ else
79
+ add_appointment_event(appointment_map, appointments, phrase_builder, summary_words, occurrences)
80
+ end
81
+ else
82
+ if all_day_event?(event)
83
+ add_anniversary(event, anniversary_map, anniversaries, summary_words, occurrences)
84
+ else
85
+ add_appointment(event, appointment_map, appointments, summary_words, occurrences)
69
86
  end
70
87
  end
71
88
  end
72
-
73
- # Sort the appointments by time
74
- appointments.sort_by! { |appointment| appointment.time }
75
- write_to_watch(appointments)
89
+
90
+ [appointments, anniversaries]
91
+ end
92
+
93
+ def all_day_event?(event)
94
+ (event.dtend.to_date - event.dtstart.to_date == 1) && event.dtstart.to_datetime.hour == 0 && event.dtstart.to_datetime.min == 0 && event.dtend.to_datetime.hour == 0 && event.dtend.to_datetime.min == 0
95
+ end
96
+
97
+ def parse_summary(event)
98
+ event.summary.to_s.split.map do |word|
99
+ if word =~ /\A[a-zA-Z]+\z/
100
+ word
101
+ elsif word =~ /\A\d+\z/
102
+ word.to_i.humanize
103
+ end
104
+ end.compact
105
+ end
106
+
107
+ def add_appointment_event(appointment_map, appointments, phrase_builder, summary_words, occurrences)
108
+ occurrences.each do |occurrence|
109
+ est_time = occurrence.start_time.in_time_zone(get_localzone)
110
+ key = "#{est_time}_#{summary_words}"
111
+ unless appointment_map[key]
112
+ puts "Adding appointment event: #{summary_words.join(' ')} at time #{est_time}"
113
+ event_phrase = phrase_builder.vocab_ids_for(*summary_words)
114
+ appointment = @protocol_class::Eeprom::Calendar::Event.new(
115
+ time: est_time,
116
+ phrase: event_phrase
117
+ )
118
+ appointments << appointment
119
+ appointment_map[key] = true
120
+ end
121
+ end
122
+ end
123
+
124
+ def add_anniversary_event(anniversary_map, anniversaries, phrase_builder, summary_words, occurrences)
125
+ occurrences.each do |occurrence|
126
+ est_time = occurrence.start_time.in_time_zone(get_localzone)
127
+ key = "#{est_time}_#{summary_words.join(' ')}"
128
+ unless anniversary_map[key]
129
+ puts "Adding anniversary event: #{summary_words.join(' ')} at date #{event.dtstart.to_s}"
130
+ event_phrase = phrase_builder.vocab_ids_for(*summary_words)
131
+ anniversary = @protocol_class::Eeprom::Calendar::Event.new(
132
+ time: Time.new(est_time.year, est_time.month, est_time.day, 9, 30, 0),
133
+ phrase: event_phrase
134
+ )
135
+ anniversaries << anniversary
136
+ anniversary_map[key] = true
137
+ end
138
+ end
139
+ end
140
+
141
+ def add_appointment(event, appointment_map, appointments, summary_words, occurrences)
142
+ occurrences.each do |occurrence|
143
+ est_time = occurrence.start_time.in_time_zone(get_localzone)
144
+ key = "#{est_time}_#{summary_words}"
145
+ unless appointment_map[key]
146
+ puts "Adding appointment: #{summary_words} at time #{est_time}"
147
+ appointment = @protocol_class::Eeprom::Appointment.new(
148
+ time: est_time,
149
+ message: summary_words
150
+ )
151
+ appointments << appointment
152
+ appointment_map[key] = true
153
+ end
154
+ end
155
+ end
156
+
157
+ def add_anniversary(event, anniversary_map, anniversaries, summary_words, occurrences)
158
+ occurrences.each do |occurrence|
159
+ est_time = occurrence.start_time.in_time_zone(get_localzone)
160
+ key = "#{est_time}_#{summary_words}"
161
+ unless anniversary_map[key]
162
+ puts "Adding anniversary: #{summary_words} at date #{event.dtstart.to_s}"
163
+ anniversary = @protocol_class::Eeprom::Anniversary.new(
164
+ time: event.dtstart.to_time,
165
+ anniversary: summary_words
166
+ )
167
+ anniversaries << anniversary
168
+ anniversary_map[key] = true
169
+ end
170
+ end
76
171
  end
77
172
 
78
- def write_to_watch(appointments)
173
+ def write_to_watch(appointments, anniversaries)
174
+ appointments.sort_by! { |appointment| appointment.time }
79
175
  # add 3 because it always seems to be about 3 seconds behind.
80
176
  time1 = Time.now + 3
81
177
  time2 = time1.dup.utc
@@ -99,7 +195,7 @@ module TimexDatalinkCaldav
99
195
  zone: 2,
100
196
  name: time2.zone
101
197
  )
102
- else
198
+ elsif @protocol_version == 3 || @protocol_version == 4
103
199
  time_model = @protocol_class::Time.new(
104
200
  zone: 1,
105
201
  name: time1.zone,
@@ -117,21 +213,41 @@ module TimexDatalinkCaldav
117
213
  time_name_model = nil # Not needed for protocol version 3 and 4
118
214
  utc_time_name_model = nil # Not needed for protocol version 3 and 4
119
215
 
216
+ else
217
+ time_model = nil
218
+ utc_time_model = nil
219
+ time_name_model = nil
220
+ utc_time_name_model = nil
221
+ end
222
+
223
+ if @protocol_version == 7
224
+ calendar = @protocol_class::Eeprom::Calendar.new(
225
+ time: time1,
226
+ events: appointments
227
+ )
228
+ models = [
229
+ @protocol_class::Sync.new,
230
+ @protocol_class::Start.new,
231
+ @protocol_class::Eeprom.new(
232
+ calendar: calendar
233
+ )
234
+ ]
235
+ else
236
+ models = [
237
+ @protocol_class::Sync.new,
238
+ @protocol_class::Start.new,
239
+ time_model,
240
+ time_name_model,
241
+ utc_time_model,
242
+ utc_time_name_model,
243
+ @protocol_class::Eeprom.new(
244
+ appointments: appointments,
245
+ anniversaries: anniversaries,
246
+ appointment_notification_minutes: 5
247
+ ),
248
+ @protocol_class::End.new
249
+ ].compact # Remove any nil entries
120
250
  end
121
-
122
- models = [
123
- @protocol_class::Sync.new,
124
- @protocol_class::Start.new,
125
- time_model,
126
- time_name_model,
127
- utc_time_model,
128
- utc_time_name_model,
129
- @protocol_class::Eeprom.new(
130
- appointments: appointments,
131
- appointment_notification_minutes: 5
132
- ),
133
- @protocol_class::End.new
134
- ].compact # Remove any nil entries
135
251
 
136
252
  timex_datalink_client = TimexDatalinkClient.new(
137
253
  serial_device: @serial_device,
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mdb"
4
+ require "text" # import the text gem
5
+
6
+
7
+ class SimlarWord
8
+ class WordNotFound < StandardError; end
9
+
10
+ attr_accessor :database
11
+
12
+ def initialize(database:)
13
+ @database = database
14
+ end
15
+
16
+ def vocab_ids_for(*words)
17
+ words.flat_map do |word|
18
+ vocab = vocab_for_word(word)
19
+
20
+ # If the word is not found, look for a similar sounding word
21
+ if vocab.nil?
22
+ word = find_similar_word(word)
23
+ vocab = vocab_for_word(word)
24
+ end
25
+
26
+ raise(WordNotFound, "#{word} is not a valid word!") unless vocab
27
+
28
+ vocab_links = vocab_links_for_vocab(vocab)
29
+
30
+ vocab_links.map do |vocab_link|
31
+ linked_vocab = vocab_for_vocab_link(vocab_link)
32
+
33
+ linked_vocab[:"PC Index"].to_i
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def find_similar_word(word)
41
+ word_metaphone = Text::Metaphone.double_metaphone(word)
42
+
43
+ # Filter out words that start with '<' before finding the minimum distance
44
+ filtered_vocab_table = vocab_table.reject { |vocab_word| vocab_word[:Label].start_with?('<') }
45
+
46
+ similar_word = filtered_vocab_table.min_by do |vocab_word|
47
+ distance = Text::Levenshtein.distance(
48
+ Text::Metaphone.double_metaphone(vocab_word[:Label]).first,
49
+ word_metaphone.first
50
+ )
51
+ distance
52
+ end
53
+
54
+ if similar_word
55
+ puts "Similar word for '#{word}' is '#{similar_word[:Label]}'"
56
+ similar_word[:Label]
57
+ else
58
+ puts "No similar word found for '#{word}', using original word"
59
+ word
60
+ end
61
+ end
62
+
63
+ def mdb
64
+ @mdb ||= Mdb.open(database)
65
+ end
66
+
67
+ def vocab_table
68
+ @vocab_table ||= mdb["Vocab"]
69
+ end
70
+
71
+ def vocab_links_table
72
+ @vocab_links_table ||= mdb["Vocab Links"]
73
+ end
74
+
75
+ def vocab_for_word(word)
76
+ vocab_table.detect { |vocab| vocab[:Label].casecmp?(word) }
77
+ end
78
+
79
+ def vocab_links_for_vocab(vocab)
80
+ links = vocab_links_table.select { |vocab_link| vocab_link[:"PC Index"] == vocab[:"PC Index"] }
81
+
82
+ links.sort_by { |link| link[:Sequence].to_i }
83
+ end
84
+
85
+ def vocab_for_vocab_link(vocab_link)
86
+ vocab_table.detect { |vocab| vocab[:"PC Index"] == vocab_link[:"eBrain Index"] }
87
+ end
88
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TimexDatalinkCaldav
4
- VERSION = "0.3.0"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -1,5 +1,6 @@
1
- # frozen_string_literal: true
2
1
  require 'optparse'
2
+ require 'yaml'
3
+ require 'open-uri'
3
4
  require_relative "timex_datalink_caldav/client"
4
5
 
5
6
  module TimexDatalinkCaldav
@@ -9,27 +10,55 @@ module TimexDatalinkCaldav
9
10
  end
10
11
 
11
12
  def execute
12
- client = TimexDatalinkCaldav::Client.new(@options.fetch(:user), @options.fetch(:password), @options.fetch(:uri), @options.fetch(:device), @options.fetch(:api,1))
13
- client.sync_to_watch
13
+ all_appointments = []
14
+ all_anniversaries = []
15
+
16
+ @options[:endpoints].each do |endpoint|
17
+ client = TimexDatalinkCaldav::Client.new(endpoint[:user], endpoint[:password], endpoint[:uri], @options[:device], @options[:api], @options[:days_forward])
18
+ appointments, anniversaries = client.parse_events
19
+ all_appointments.concat(appointments) if appointments.any?
20
+ all_anniversaries.concat(anniversaries) if anniversaries.any?
21
+ end
22
+
23
+ if all_appointments.any? || all_anniversaries.any?
24
+ client = TimexDatalinkCaldav::Client.new(
25
+ @options[:endpoints][0][:user],
26
+ @options[:endpoints][0][:password],
27
+ @options[:endpoints][0][:uri],
28
+ @options[:device],
29
+ @options[:api],
30
+ @options[:days_forward]
31
+ )
32
+ client.write_to_watch(all_appointments, all_anniversaries)
33
+ end
14
34
  end
15
35
 
16
36
  private
17
37
 
18
38
  def parse_options(arguments)
19
- options = {}
39
+ options = { days_forward: 1 }
40
+ cli_endpoint = {}
20
41
  OptionParser.new do |opts|
21
42
  opts.banner = "Usage: timex_datalink_caldav [options]"
22
43
 
44
+ opts.on("-c", "--config FILE", "Configuration file") do |v|
45
+ raise ArgumentError, "Both CLI options and configuration file provided. Please provide only one." if cli_endpoint.any?
46
+ parsed_config = YAML.load_file(v)
47
+ options[:endpoints] = parsed_config['endpoints'].map do |endpoint|
48
+ endpoint.each_with_object({}) { |(k, v), result| result[k.to_sym] = v }
49
+ end
50
+ end
51
+
23
52
  opts.on("-u", "--uri URI", "CalDAV server URI") do |v|
24
- options[:uri] = v
53
+ cli_endpoint[:uri] = v
25
54
  end
26
55
 
27
56
  opts.on("-n", "--user USERNAME", "Username for CalDAV server") do |v|
28
- options[:user] = v
57
+ cli_endpoint[:user] = v
29
58
  end
30
59
 
31
60
  opts.on("-p", "--password PASSWORD", "Password for CalDAV server") do |v|
32
- options[:password] = v
61
+ cli_endpoint[:password] = v
33
62
  end
34
63
 
35
64
  opts.on("-d", "--device DEVICE", "Serial device for Timex Datalink watch") do |v|
@@ -39,8 +68,14 @@ module TimexDatalinkCaldav
39
68
  opts.on("-a", "--api PROTOCOL_VERSION", "Protocol Version") do |v|
40
69
  options[:api] = v
41
70
  end
71
+
72
+ opts.on("-f", "--forward DAYS", Integer, "Number of days to look forward for events") do |v|
73
+ options[:days_forward] = v
74
+ end
42
75
  end.parse!(arguments)
76
+
77
+ options[:endpoints] = [cli_endpoint] if cli_endpoint.any? and options[:endpoints].nil?
43
78
  options
44
79
  end
45
80
  end
46
- end
81
+ end
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  end
29
29
  spec.require_paths = ["lib"]
30
30
  spec.executables << 'timex_datalink_caldav'
31
-
31
+ spec.executables << 'get_ebrain_db'
32
32
  # Uncomment to register a new dependency of your gem
33
33
  # spec.add_dependency "example-gem", "~> 1.0"
34
34
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timex_datalink_caldav
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willy Hardy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-17 00:00:00.000000000 Z
11
+ date: 2023-05-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Adds a CLI and a feature to pull your next day of calendar events into
14
14
  the Timex Datalink watch. Note: Hardcoded protocol1 and EST timezone. At the moment.'
@@ -16,17 +16,23 @@ email:
16
16
  - zpga8gbp@mailer.me
17
17
  executables:
18
18
  - timex_datalink_caldav
19
+ - get_ebrain_db
19
20
  extensions: []
20
21
  extra_rdoc_files: []
21
22
  files:
22
23
  - CHANGELOG.md
23
24
  - Gemfile
24
25
  - Gemfile.lock
26
+ - LICENSE
25
27
  - README.md
26
28
  - Rakefile
29
+ - bin/get_ebrain_db
27
30
  - bin/timex_datalink_caldav
31
+ - examples/README.md
32
+ - examples/caldav.yaml.sample
28
33
  - lib/timex_datalink_caldav.rb
29
34
  - lib/timex_datalink_caldav/client.rb
35
+ - lib/timex_datalink_caldav/similar_word.rb
30
36
  - lib/timex_datalink_caldav/version.rb
31
37
  - sig/timex_datalink_caldav.rbs
32
38
  - timex_datalink_caldav.gemspec
@@ -53,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
59
  - !ruby/object:Gem::Version
54
60
  version: '0'
55
61
  requirements: []
56
- rubygems_version: 3.4.10
62
+ rubygems_version: 3.4.13
57
63
  signing_key:
58
64
  specification_version: 4
59
65
  summary: Allows the Timex Datalink watch to sync with a CalDAV server.