SMACC2
Loading...
Searching...
No Matches
cp_calendar_poller.cpp
Go to the documentation of this file.
1// Copyright 2024 RobosoftAI Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
16
17#include <boost/regex.hpp>
18#include <iomanip>
19#include <sstream>
20
21namespace cl_gcalcli
22{
23
24CpCalendarPoller::CpCalendarPoller() : connection_(nullptr), initialized_(false) {}
25
27{
28 if (!initialized_)
29 {
31
32 if (connection_ == nullptr)
33 {
34 RCLCPP_ERROR(getLogger(), "[CpCalendarPoller] CpGcalcliConnection component not found!");
35 return;
36 }
37
38 last_poll_attempt_ = std::chrono::steady_clock::now();
39 initialized_ = true;
40
41 RCLCPP_INFO(getLogger(), "[CpCalendarPoller] Initialized");
42 }
43}
44
46{
48 {
49 RCLCPP_WARN(getLogger(), "[CpCalendarPoller] Cannot refresh - not connected");
50 return false;
51 }
52
53 const auto & config = connection_->getConfig();
54
55 // Build the agenda command with TSV output
56 std::stringstream args;
57 args << "agenda --tsv --nocolor";
58 args << " --nodeclined";
59
60 // Add date range
61 auto now = std::chrono::system_clock::now();
62 auto end = now + std::chrono::hours(24 * config.agenda_days);
63
64 auto now_t = std::chrono::system_clock::to_time_t(now);
65 auto end_t = std::chrono::system_clock::to_time_t(end);
66
67 std::tm now_tm = *std::localtime(&now_t);
68 std::tm end_tm = *std::localtime(&end_t);
69
70 args << " \"" << std::put_time(&now_tm, "%m/%d/%Y") << "\"";
71 args << " \"" << std::put_time(&end_tm, "%m/%d/%Y") << "\"";
72
73 auto result = connection_->executeGcalcli(args.str(), 30000);
74
75 if (result.exit_code != 0 || result.timed_out)
76 {
77 RCLCPP_WARN(
78 getLogger(), "[CpCalendarPoller] Agenda fetch failed: %s", result.stdout_output.c_str());
79 return false;
80 }
81
82 // Parse the TSV output
83 auto events = parseTsvOutput(result.stdout_output);
84
85 {
86 std::lock_guard<std::mutex> lock(events_mutex_);
87 cached_events_ = events;
88 last_poll_time_ = std::chrono::system_clock::now();
89 }
90
91 RCLCPP_DEBUG(getLogger(), "[CpCalendarPoller] Refreshed agenda: %zu events", events.size());
92
93 // Emit signal and post event
94 onAgendaUpdated_(events);
96 {
98 }
99
100 return true;
101}
102
103std::vector<CalendarEvent> CpCalendarPoller::getEvents() const
104{
105 std::lock_guard<std::mutex> lock(events_mutex_);
106 return cached_events_;
107}
108
109std::vector<CalendarEvent> CpCalendarPoller::findEvents(
110 const std::string & pattern, bool use_regex) const
111{
112 std::lock_guard<std::mutex> lock(events_mutex_);
113
114 std::vector<CalendarEvent> matches;
115
116 if (use_regex)
117 {
118 try
119 {
120 boost::regex regex(pattern, boost::regex::icase);
121 for (const auto & event : cached_events_)
122 {
123 if (boost::regex_search(event.title, regex))
124 {
125 matches.push_back(event);
126 }
127 }
128 }
129 catch (const boost::regex_error & e)
130 {
131 RCLCPP_ERROR(getLogger(), "[CpCalendarPoller] Invalid regex pattern: %s", e.what());
132 }
133 }
134 else
135 {
136 for (const auto & event : cached_events_)
137 {
138 if (event.title.find(pattern) != std::string::npos)
139 {
140 matches.push_back(event);
141 }
142 }
143 }
144
145 return matches;
146}
147
148std::vector<CalendarEvent> CpCalendarPoller::getEventsInWindow(
149 std::chrono::system_clock::time_point start, std::chrono::system_clock::time_point end) const
150{
151 std::lock_guard<std::mutex> lock(events_mutex_);
152
153 std::vector<CalendarEvent> matches;
154 for (const auto & event : cached_events_)
155 {
156 // Event overlaps with window if it starts before window end and ends after window start
157 if (event.start_time < end && event.end_time > start)
158 {
159 matches.push_back(event);
160 }
161 }
162
163 return matches;
164}
165
166std::vector<CalendarEvent> CpCalendarPoller::getActiveEvents() const
167{
168 std::lock_guard<std::mutex> lock(events_mutex_);
169
170 std::vector<CalendarEvent> active;
171 for (const auto & event : cached_events_)
172 {
173 if (event.isActiveNow())
174 {
175 active.push_back(event);
176 }
177 }
178
179 return active;
180}
181
182std::chrono::system_clock::time_point CpCalendarPoller::getLastPollTime() const
183{
184 std::lock_guard<std::mutex> lock(events_mutex_);
185 return last_poll_time_;
186}
187
189{
190 if (!initialized_ || !connection_)
191 {
192 return;
193 }
194
195 if (!connection_->isConnected())
196 {
197 return;
198 }
199
200 const auto & config = connection_->getConfig();
201 auto now = std::chrono::steady_clock::now();
202 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - last_poll_attempt_);
203
204 if (elapsed >= config.poll_interval)
205 {
207 last_poll_attempt_ = now;
208 }
209}
210
211std::vector<CalendarEvent> CpCalendarPoller::parseTsvOutput(const std::string & output)
212{
213 std::vector<CalendarEvent> events;
214 std::istringstream stream(output);
215 std::string line;
216
217 while (std::getline(stream, line))
218 {
219 // Skip empty lines
220 if (line.empty() || line.find_first_not_of(" \t\r\n") == std::string::npos)
221 {
222 continue;
223 }
224
225 auto event = parseTsvLine(line);
226 if (event.has_value())
227 {
228 events.push_back(event.value());
229 }
230 }
231
232 return events;
233}
234
235std::optional<CalendarEvent> CpCalendarPoller::parseTsvLine(const std::string & line)
236{
237 // TSV columns: Start_Date | Start_Time | End_Date | End_Time | Title | Location | Description | Calendar
238 std::vector<std::string> fields;
239 std::istringstream stream(line);
240 std::string field;
241
242 while (std::getline(stream, field, '\t'))
243 {
244 fields.push_back(field);
245 }
246
247 // We need at least 5 fields (date/time + title)
248 if (fields.size() < 5)
249 {
250 RCLCPP_DEBUG(
251 getLogger(), "[CpCalendarPoller] Skipping line with insufficient fields: %s", line.c_str());
252 return std::nullopt;
253 }
254
255 CalendarEvent event;
256
257 // Parse start time
258 auto start = parseDateTime(fields[0], fields[1]);
259 if (!start.has_value())
260 {
261 RCLCPP_DEBUG(
262 getLogger(), "[CpCalendarPoller] Failed to parse start time: %s %s", fields[0].c_str(),
263 fields[1].c_str());
264 return std::nullopt;
265 }
266 event.start_time = start.value();
267
268 // Parse end time
269 auto end = parseDateTime(fields[2], fields[3]);
270 if (!end.has_value())
271 {
272 RCLCPP_DEBUG(
273 getLogger(), "[CpCalendarPoller] Failed to parse end time: %s %s", fields[2].c_str(),
274 fields[3].c_str());
275 return std::nullopt;
276 }
277 event.end_time = end.value();
278
279 // Title (required)
280 event.title = fields[4];
281
282 // Optional fields
283 if (fields.size() > 5) event.location = fields[5];
284 if (fields.size() > 6) event.description = fields[6];
285 if (fields.size() > 7) event.calendar_name = fields[7];
286
287 // Check for all-day event (typically has no time or 00:00)
288 event.is_all_day = (fields[1].empty() || fields[1] == "00:00" || fields[1] == "12:00am");
289
290 // Generate ID
291 event.id = generateEventId(event);
292
293 return event;
294}
295
296std::optional<std::chrono::system_clock::time_point> CpCalendarPoller::parseDateTime(
297 const std::string & date_str, const std::string & time_str)
298{
299 std::tm tm = {};
300
301 // Parse date - gcalcli TSV uses YYYY-MM-DD format
302 std::istringstream date_stream(date_str);
303 char sep1, sep2;
304 int year, month, day;
305
306 if (date_stream >> year >> sep1 >> month >> sep2 >> day && sep1 == '-' && sep2 == '-')
307 {
308 // YYYY-MM-DD format (gcalcli TSV)
309 tm.tm_year = year - 1900;
310 tm.tm_mon = month - 1;
311 tm.tm_mday = day;
312 }
313 else
314 {
315 // Try MM/DD/YYYY format as fallback
316 date_stream.clear();
317 date_stream.str(date_str);
318 if (date_stream >> month >> sep1 >> day >> sep2 >> year && sep1 == '/' && sep2 == '/')
319 {
320 tm.tm_mon = month - 1;
321 tm.tm_mday = day;
322 tm.tm_year = year - 1900;
323 }
324 else
325 {
326 RCLCPP_DEBUG(getLogger(), "[CpCalendarPoller] Failed to parse date: %s", date_str.c_str());
327 return std::nullopt;
328 }
329 }
330
331 // Parse time - gcalcli TSV uses 24-hour HH:MM format
332 if (!time_str.empty())
333 {
334 std::istringstream time_stream(time_str);
335 char colon;
336 int hour, minute;
337
338 if (time_stream >> hour >> colon >> minute && colon == ':')
339 {
340 tm.tm_hour = hour;
341 tm.tm_min = minute;
342
343 // Check for am/pm suffix (for non-TSV formats)
344 std::string rest;
345 std::getline(time_stream, rest);
346 if (!rest.empty())
347 {
348 // Remove leading spaces
349 size_t start = rest.find_first_not_of(" ");
350 if (start != std::string::npos)
351 {
352 rest = rest.substr(start);
353 }
354 // Check for am/pm
355 if (rest == "pm" || rest == "PM" || rest == "p" || rest == "P")
356 {
357 if (hour != 12) tm.tm_hour += 12;
358 }
359 else if (rest == "am" || rest == "AM" || rest == "a" || rest == "A")
360 {
361 if (hour == 12) tm.tm_hour = 0;
362 }
363 }
364 }
365 else
366 {
367 RCLCPP_DEBUG(getLogger(), "[CpCalendarPoller] Failed to parse time: %s", time_str.c_str());
368 return std::nullopt;
369 }
370 }
371
372 tm.tm_sec = 0;
373 tm.tm_isdst = -1; // Let mktime determine DST
374
375 std::time_t time = std::mktime(&tm);
376 if (time == -1)
377 {
378 return std::nullopt;
379 }
380
381 return std::chrono::system_clock::from_time_t(time);
382}
383
385{
386 // Generate a unique ID based on title and start time
387 auto start_t = std::chrono::system_clock::to_time_t(event.start_time);
388 std::stringstream ss;
389 ss << event.title << "_" << start_t << "_" << event.calendar_name;
390 return ss.str();
391}
392
393} // namespace cl_gcalcli
std::optional< std::chrono::system_clock::time_point > parseDateTime(const std::string &date_str, const std::string &time_str)
Parse date and time strings into time_point.
smacc2::SmaccSignal< void(const std::vector< CalendarEvent > &)> onAgendaUpdated_
std::vector< CalendarEvent > getEventsInWindow(std::chrono::system_clock::time_point start, std::chrono::system_clock::time_point end) const
Get events happening within a time window.
std::chrono::system_clock::time_point last_poll_time_
bool refreshAgenda()
Force an immediate agenda refresh.
void update() override
Periodic update for polling (called by SignalDetector)
std::vector< CalendarEvent > findEvents(const std::string &pattern, bool use_regex=false) const
Get events matching a title pattern.
std::vector< CalendarEvent > getActiveEvents() const
Get currently active events.
std::vector< CalendarEvent > getEvents() const
Get cached list of calendar events.
std::string generateEventId(const CalendarEvent &event)
Generate a unique ID for an event.
std::optional< CalendarEvent > parseTsvLine(const std::string &line)
Parse a single TSV line into a CalendarEvent.
std::chrono::steady_clock::time_point last_poll_attempt_
std::function< void(const std::vector< CalendarEvent > &)> postAgendaUpdatedEvent_
std::vector< CalendarEvent > parseTsvOutput(const std::string &output)
Parse TSV output into CalendarEvent structures.
std::chrono::system_clock::time_point getLastPollTime() const
Get the time of the last successful poll.
std::vector< CalendarEvent > cached_events_
const GcalcliConfig & getConfig() const
Get the gcalcli configuration.
bool isConnected() const
Check if connected to Google Calendar.
smacc2::client_core_components::SubprocessResult executeGcalcli(const std::string &args, int timeout_ms=30000)
Execute a gcalcli command.
rclcpp::Logger getLogger() const
void requiresComponent(TComponent *&requiredComponentStorage, ComponentRequirement requirementType=ComponentRequirement::SOFT)
Represents a Google Calendar event.
Definition types.hpp:40
std::chrono::system_clock::time_point start_time
Definition types.hpp:46