Line data Source code
1 : // Webthing-CPP
2 : // SPDX-FileCopyrightText: 2023-present Benno Waldhauer
3 : // SPDX-License-Identifier: MIT
4 :
5 : #pragma once
6 :
7 : #include <vector>
8 : #include <bw/webthing/action.hpp>
9 : #include <bw/webthing/constants.hpp>
10 : #include <bw/webthing/event.hpp>
11 : #include <bw/webthing/json.hpp>
12 : #include <bw/webthing/property.hpp>
13 : #include <bw/webthing/storage.hpp>
14 :
15 : namespace bw::webthing {
16 :
17 : class Thing
18 : {
19 : public:
20 : typedef std::function<std::shared_ptr<Action> (std::optional<json> input)> ActionSupplier;
21 : struct AvailableAction
22 : {
23 : json metadata;
24 : ActionSupplier class_supplier;
25 : };
26 :
27 : typedef std::function<void(const std::string& /*topic*/, const json& /*message*/)> MessageCallback;
28 :
29 36 : Thing(std::string id, std::string title, std::vector<std::string> type, std::string description = "")
30 108 : : id(id), title(title), type(type), description(description)
31 : {
32 36 : }
33 :
34 7 : Thing(std::string id, std::string title, std::string type = "", std::string description = "")
35 28 : : Thing(id, title, std::vector<std::string>{type}, description)
36 : {
37 14 : }
38 :
39 19 : json as_thing_description() const
40 : {
41 : json thing({
42 19 : {"id", id},
43 19 : {"title", title},
44 19 : {"@context", context},
45 19 : {"@type", type},
46 19 : {"properties", get_property_descriptions() },
47 19 : {"actions", json::object()},
48 19 : {"events", json::object()},
49 19 : {"description", description},
50 : {"links", {
51 19 : {{"rel", "properties"}, {"href", href_prefix + "/properties"}},
52 19 : {{"rel", "actions"}, {"href", href_prefix + "/actions"}},
53 19 : {{"rel", "events"}, {"href", href_prefix + "/events"}}
54 : }}
55 1026 : });
56 :
57 19 : if(ui_href)
58 : {
59 11 : thing["links"].push_back({
60 : {"rel", "alternate"}, {"mediaType", "text/html"}, {"href", *ui_href}
61 : });
62 : }
63 :
64 23 : for(auto& aa : available_actions)
65 : {
66 4 : std::string name = aa.first;
67 4 : thing["actions"][name] = aa.second.metadata;
68 44 : thing["actions"][name]["links"] = {{{"rel", "action"}, {"href", href_prefix + "/actions/" + name}}};
69 0 : }
70 :
71 23 : for(auto& ae : available_events)
72 : {
73 4 : std::string name = ae.first;
74 4 : thing["events"][name] = ae.second;
75 44 : thing["events"][name]["links"] = {{{"rel", "event"}, {"href", href_prefix + "/events/" + name}}};
76 0 : }
77 19 : return thing;
78 1084 : }
79 :
80 71 : std::string get_href() const
81 : {
82 71 : if(href_prefix.size() > 0)
83 38 : return href_prefix;
84 66 : return "/";
85 : }
86 :
87 2 : std::optional<std::string> get_ui_href() const
88 : {
89 2 : return ui_href;
90 : }
91 :
92 1 : void set_ui_href(std::string href)
93 : {
94 1 : ui_href = href;
95 1 : }
96 :
97 21 : std::string get_id() const
98 : {
99 21 : return id;
100 : }
101 :
102 11 : std::string get_title() const
103 : {
104 11 : return title;
105 : }
106 :
107 1 : std::string get_description() const
108 : {
109 1 : return description;
110 : }
111 :
112 2 : std::vector<std::string> get_type() const
113 : {
114 2 : return type;
115 : }
116 :
117 2 : std::string get_context() const
118 : {
119 2 : return context;
120 : }
121 :
122 1 : void set_context(std::string context)
123 : {
124 1 : this->context = context;
125 1 : }
126 :
127 19 : json get_property_descriptions() const
128 : {
129 19 : auto pds = json::object();
130 36 : for(const auto& p : properties)
131 17 : pds[p.first] = p.second->as_property_description();
132 19 : return pds;
133 0 : }
134 :
135 : // Get the thing's actions as json array
136 : // action_name -- Optional action name to get description for
137 8 : json get_action_descriptions(std::optional<std::string> action_name = std::nullopt) const
138 : {
139 8 : json descriptions = json::array();
140 :
141 21 : for(const auto& action_entry : actions)
142 20 : for(const auto& action : action_entry.second)
143 7 : if(!action_name || action_name == action_entry.first)
144 7 : descriptions.push_back(action->as_action_description());
145 :
146 8 : return descriptions;
147 0 : }
148 :
149 : // Get the thing's events as json array.
150 : // event_name -- Optional event name to get description for
151 15 : json get_event_descriptions(const std::optional<std::string>& event_name = std::nullopt) const
152 : {
153 15 : json descriptions = json::array();
154 :
155 55 : for(const auto& evt : events)
156 40 : if(!event_name || event_name == evt->get_name())
157 23 : descriptions.push_back(evt->as_event_description());
158 :
159 15 : return descriptions;
160 0 : }
161 :
162 17 : void add_property(std::shared_ptr<PropertyBase> property)
163 : {
164 17 : property->set_href_prefix(href_prefix);
165 17 : properties[property->get_name()] = property;
166 17 : }
167 :
168 : void remove_property(const PropertyBase& property)
169 : {
170 : properties.erase(property.get_name());
171 : }
172 :
173 : // Find a property by name
174 35 : std::shared_ptr<PropertyBase> find_property(std::string property_name) const
175 : {
176 35 : if(properties.count(property_name) > 0)
177 32 : return properties.at(property_name);
178 3 : return nullptr;
179 : }
180 :
181 : template<class T>
182 17 : void set_property(std::string property_name, T value)
183 : {
184 17 : auto prop = find_property(property_name);
185 17 : if(prop)
186 18 : prop->set_value(value);
187 17 : }
188 :
189 : template<class T>
190 5 : std::optional<T> get_property(std::string property_name) const
191 : {
192 5 : auto property = find_property(property_name);
193 5 : if(property)
194 5 : return property->get_value<T>();
195 0 : return std::nullopt;
196 5 : }
197 :
198 : // Get a mapping of all properties and their values.
199 4 : json get_properties() const
200 : {
201 4 : auto json = json::object();
202 :
203 11 : for(const auto& pe : properties)
204 : {
205 7 : json[pe.first] = pe.second->get_property_value_object()[pe.first];
206 : }
207 4 : return json;
208 0 : }
209 :
210 : //Determine whether or not this thing has a given property.
211 : // property_name -- the property to look for
212 : bool has_property(std::string property_name) const
213 : {
214 : return properties.find(property_name) != properties.end();
215 : }
216 :
217 17 : void property_notify(json property_status_message)
218 : {
219 17 : logger::debug("thing::property_notify : " + property_status_message.dump());
220 30 : for(auto& observer : observers)
221 13 : observer( id + "/properties", property_status_message);
222 17 : }
223 :
224 : // Perform an action on the thing.
225 : // name -- name of the action
226 : // input -- any action inputs
227 28 : std::shared_ptr<Action> perform_action(std::string name, std::optional<json> input = std::nullopt)
228 : {
229 28 : if(available_actions.count(name) == 0)
230 1 : return nullptr;
231 :
232 27 : auto& action_type = available_actions[name];
233 :
234 27 : if(action_type.metadata.contains("input"))
235 : {
236 : try
237 : {
238 37 : validate_value_by_scheme(input.value_or(json()), action_type.metadata["input"]);
239 : }
240 8 : catch(std::exception& ex)
241 : {
242 24 : logger::debug("action: '" + name + "' invalid input: " +
243 32 : input.value_or(json()).dump() +" error: " + ex.what());
244 8 : return nullptr;
245 8 : }
246 : }
247 :
248 : try
249 : {
250 21 : auto action = action_type.class_supplier( std::move(input) );
251 17 : action->set_href_prefix(href_prefix);
252 17 : action_notify(action_status_message(action));
253 17 : actions[name].add(action);
254 17 : return action;
255 17 : }
256 2 : catch(std::exception& ex)
257 : {
258 2 : logger::debug("Construction of action '" + name + "' failed with error: " + ex.what());
259 2 : return nullptr;
260 2 : }
261 : }
262 :
263 : // Add an available action.
264 : // name -- name of the action
265 : // metadata -- action metadata, i.e. type, description, etc. as a json object
266 : // class_supplier -- function to instantiate this action
267 16 : void add_available_action(std::string name, json metadata, ActionSupplier class_supplier)
268 : {
269 16 : if(!metadata.is_object())
270 3 : throw ActionError("Action metadata must be encoded as json object.");
271 :
272 15 : available_actions[name] = { metadata, class_supplier };
273 15 : actions[name] = {action_storage_config};
274 30 : }
275 :
276 33 : void action_notify(json action_status_message)
277 : {
278 33 : logger::debug("thing::action_notify : " + action_status_message.dump());
279 42 : for(auto& observer : observers)
280 9 : observer( id + "/actions", action_status_message);
281 33 : }
282 :
283 : // Get an action by its name and id
284 : // return the action when found, std::nullopt otherwise
285 11 : std::shared_ptr<Action> get_action(std::string action_name, std::string action_id) const
286 : {
287 11 : if(actions.count(action_name) == 0)
288 2 : return nullptr;
289 :
290 9 : const auto& actions_for_name = actions.at(action_name);
291 13 : for(const auto& action : actions_for_name)
292 10 : if(action->get_id() == action_id)
293 6 : return action;
294 :
295 3 : return nullptr;
296 : }
297 :
298 : // Remove an existing action identified by its name and id
299 : // Returns bool indicating the presence of the action
300 2 : bool remove_action(std::string action_name, std::string action_id)
301 : {
302 2 : auto action = get_action(action_name, action_id);
303 2 : if(!action)
304 0 : return false;
305 :
306 2 : action->cancel();
307 2 : auto& as = actions[action_name];
308 6 : as.remove_if([&action_id](auto a){return a->get_id() == action_id;});
309 2 : return true;
310 2 : }
311 :
312 : // Add a new event and notify subscribers
313 14 : void add_event(std::shared_ptr<Event> event)
314 : {
315 14 : events.add(event);
316 14 : event_notify(*event);
317 14 : }
318 :
319 : // Add an available event.
320 : // name -- name of the event
321 : // metadata -- event metadata, i.e. type, description, etc., as a json object
322 11 : void add_available_event(std::string name, json metadata = json::object())
323 : {
324 11 : if(!metadata.is_object())
325 3 : throw EventError("Event metadata must be encoded as json object.");
326 :
327 10 : available_events[name] = metadata;
328 10 : }
329 :
330 14 : void event_notify(const Event& event)
331 : {
332 14 : if(available_events.count(event.get_name()) == 0)
333 0 : return;
334 :
335 14 : json message = event_message(event);
336 14 : logger::debug("thing::event_notify : " + message.dump());
337 :
338 21 : for(auto& observer : observers)
339 7 : observer( id + "/events/" + event.get_name(), message);
340 14 : }
341 :
342 : // Set the prefix of any hrefs associated with this thing.
343 17 : void set_href_prefix(std::string prefix)
344 : {
345 17 : href_prefix = prefix;
346 :
347 29 : for(const auto& property : properties )
348 12 : property.second->set_href_prefix(prefix);
349 :
350 20 : for(auto& action_entry : actions)
351 3 : for(auto& action : action_entry.second)
352 0 : action->set_href_prefix(prefix);
353 17 : }
354 :
355 17 : void add_message_observer(MessageCallback observer)
356 : {
357 17 : observers.push_back(observer);
358 17 : }
359 :
360 : // configures the storage of events, should be set in initialization phase
361 : void configure_event_storage(const StorageConfig& config)
362 : {
363 : event_storage_config = config;
364 : events = {event_storage_config};
365 : }
366 :
367 : // configures the storage of actions, should be set in initialization phase
368 : // before actions are linked to the thing
369 : void configure_action_storage(const StorageConfig& config)
370 : {
371 : action_storage_config = config;
372 : for (auto& [action_name, actions] : actions)
373 : {
374 : actions = {action_storage_config};
375 : }
376 : }
377 :
378 : protected:
379 : std::string id;
380 : std::string context = WEBTHINGS_IO_CONTEXT;
381 : std::string title;
382 : std::vector<std::string> type;
383 : std::string description;
384 : std::map<std::string, std::shared_ptr<PropertyBase>> properties;
385 : std::map<std::string, AvailableAction> available_actions;
386 : std::map<std::string, json> available_events;
387 : StorageConfig action_storage_config = {10000};
388 : std::map<std::string, FlexibleRingBuffer<std::shared_ptr<Action>>> actions;
389 : StorageConfig event_storage_config = {100000};
390 : SimpleRingBuffer<std::shared_ptr<Event>> events = {event_storage_config};
391 : std::string href_prefix;
392 : std::optional<std::string> ui_href;
393 : std::vector<MessageCallback> observers;
394 : };
395 :
396 : } // bw::webthing
|