Skip to content

Commit 1fc0c7e

Browse files
committed
feat(place/demo/lockers): implement a virtual locker system
1 parent a76323c commit 1fc0c7e

2 files changed

Lines changed: 323 additions & 0 deletions

File tree

drivers/place/demo/lockers.cr

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
require "placeos-driver"
2+
require "placeos-driver/interface/lockers"
3+
4+
class Place::Demo::Lockers < PlaceOS::Driver
5+
include Interface::Lockers
6+
alias PlaceLocker = PlaceOS::Driver::Interface::Lockers::PlaceLocker
7+
8+
descriptive_name "Locker Testing"
9+
generic_name :DemoLockers
10+
description %(used for end to end testing of locker interfaces)
11+
12+
accessor staff_api : StaffAPI_1
13+
accessor locations : LocationServices_1
14+
15+
getter building_id : String do
16+
locations.building_id.get.as_s
17+
end
18+
19+
getter levels : Array(String) do
20+
staff_api.systems_in_building(building_id).get.as_h.keys
21+
end
22+
23+
getter locker_banks : Hash(String, LockerBank) do
24+
lookup = {} of String => LockerBank
25+
levels.flat_map { |level_id|
26+
banks = lockers_details(level_id)
27+
banks.try(&.each { |bank|
28+
bank.level_id = level_id
29+
})
30+
banks
31+
}.each { |bank| lookup[bank.id] = bank }
32+
lookup
33+
end
34+
35+
class Locker
36+
include JSON::Serializable
37+
38+
getter id : String
39+
getter name : String
40+
getter bookable : Bool { false }
41+
42+
# for tracking, not part of metadata
43+
property allocated_to : String? = nil
44+
property allocated_until : Time? = nil
45+
property level_id : String? = nil
46+
property shared_with : Array(String) = [] of String
47+
48+
def release
49+
@allocated_to = nil
50+
@allocated_until = nil
51+
@shared_with = [] of String
52+
end
53+
end
54+
55+
class LockerBank
56+
include JSON::Serializable
57+
58+
getter id : String
59+
getter name : String
60+
getter lockers : Array(Locker)
61+
62+
getter locker_hash : Hash(String, Locker) do
63+
lookup = {} of String => Locker
64+
level = self.level_id
65+
lockers.each do |locker|
66+
locker.level_id = level
67+
lookup[locker.id] = locker
68+
end
69+
lookup
70+
end
71+
72+
property level_id : String? = nil
73+
end
74+
75+
def lockers_details(level_id : String) : Array(LockerBank)
76+
lockers = staff_api.metadata(level_id, "lockers").get.dig?("lockers", "details")
77+
return [] of LockerBank unless lockers
78+
Array(LockerBank).from_json(lockers.to_json)
79+
end
80+
81+
class ::PlaceOS::Driver::Interface::Lockers::PlaceLocker
82+
def initialize(@bank_id, locker : Place::Demo::Lockers::Locker, @building = nil)
83+
@locker_id = locker.id
84+
@locker_name = locker.name
85+
@mac = "lb=#{@bank_id}&lk=#{locker.id}"
86+
if time = locker.allocated_until
87+
if time > Time.utc
88+
in_use = true
89+
@expires_at = time
90+
else
91+
in_use = false
92+
@expires_at = nil
93+
end
94+
elsif allocated_to = locker.allocated_to
95+
in_use = true
96+
@expires_at = nil
97+
else
98+
in_use = false
99+
@expires_at = nil
100+
end
101+
@allocated = in_use
102+
@level = locker.level_id
103+
end
104+
end
105+
106+
# allocates a locker now, the allocation may expire
107+
@[Security(Level::Administrator)]
108+
def locker_allocate(
109+
# PlaceOS user id
110+
user_id : String,
111+
112+
# the locker location
113+
bank_id : String | Int64,
114+
115+
# allocates a random locker if this is nil
116+
locker_id : String | Int64? = nil,
117+
118+
# attempts to create a booking that expires at the time specified
119+
expires_at : Int64? = nil
120+
) : PlaceLocker
121+
locker = locker_banks[bank_id.to_s].locker_hash[locker_id.to_s]
122+
locker.allocated_to = user_id
123+
locker.allocated_until = Time.unix(expires_at) if expires_at
124+
PlaceLocker.new(bank_id, locker, building_id)
125+
end
126+
127+
# return the locker to the pool
128+
@[Security(Level::Administrator)]
129+
def locker_release(
130+
bank_id : String | Int64,
131+
locker_id : String | Int64,
132+
133+
# release / unshare just this user - otherwise release the whole locker
134+
owner_id : String? = nil
135+
) : Nil
136+
locker = locker_banks[bank_id.to_s].locker_hash[locker_id.to_s]
137+
if locker.allocated_to == owner_id
138+
locker.release
139+
else
140+
locker.shared_with.delete(owner_id)
141+
end
142+
end
143+
144+
# a list of lockers that are allocated to the user
145+
@[Security(Level::Administrator)]
146+
def lockers_allocated_to(user_id : String) : Array(PlaceLocker)
147+
now = Time.utc
148+
building = building_id
149+
150+
locker_banks.values.flat_map do |bank|
151+
bank.locker_hash.values.compact_map do |locker|
152+
if locker.allocated_to == user_id
153+
if time = locker.allocated_until
154+
PlaceLocker.new(bank.id, locker, building) if time > now
155+
else
156+
PlaceLocker.new(bank.id, locker, building)
157+
end
158+
end
159+
end
160+
end
161+
end
162+
163+
@[Security(Level::Administrator)]
164+
def locker_share(
165+
bank_id : String | Int64,
166+
locker_id : String | Int64,
167+
owner_id : String,
168+
share_with : String
169+
) : Nil
170+
locker = locker_banks[bank_id.to_s].locker_hash[locker_id.to_s]
171+
perform_share = false
172+
if locker.allocated_to == owner_id
173+
if time = locker.allocated_until
174+
perform_share = time > Time.utc
175+
else
176+
perform_share = true
177+
end
178+
end
179+
180+
if perform_share
181+
locker.shared_with << share_with
182+
locker.shared_with.uniq!
183+
end
184+
end
185+
186+
@[Security(Level::Administrator)]
187+
def locker_unshare(
188+
bank_id : String | Int64,
189+
locker_id : String | Int64,
190+
owner_id : String,
191+
# the individual you previously shared with (optional)
192+
shared_with_id : String? = nil
193+
) : Nil
194+
locker = locker_banks[bank_id.to_s].locker_hash[locker_id.to_s]
195+
perform_share = false
196+
if locker.allocated_to == owner_id
197+
if time = locker.allocated_until
198+
perform_share = time > Time.utc
199+
else
200+
perform_share = true
201+
end
202+
end
203+
204+
if perform_share
205+
if shared_with_id
206+
locker.shared_with.delete shared_with_id
207+
else
208+
locker.shared_with = [] of String
209+
end
210+
end
211+
end
212+
213+
# a list of user-ids that the locker is shared with.
214+
# this can be placeos user ids or emails
215+
@[Security(Level::Administrator)]
216+
def locker_shared_with(
217+
bank_id : String | Int64,
218+
locker_id : String | Int64,
219+
owner_id : String
220+
) : Array(String)
221+
locker = locker_banks[bank_id.to_s].locker_hash[locker_id.to_s]
222+
perform_share = false
223+
if locker.allocated_to == owner_id
224+
if time = locker.allocated_until
225+
perform_share = time > Time.utc
226+
else
227+
perform_share = true
228+
end
229+
end
230+
231+
if perform_share
232+
locker.shared_with
233+
else
234+
[] of String
235+
end
236+
end
237+
238+
@[Security(Level::Administrator)]
239+
def locker_unlock(
240+
bank_id : String | Int64,
241+
locker_id : String | Int64,
242+
243+
# sometimes required by locker systems
244+
owner_id : String? = nil,
245+
# time in seconds the locker should be unlocked
246+
# (can be ignored if not implemented)
247+
open_time : Int32 = 60,
248+
# optional pin code - if user entered from a kiosk
249+
pin_code : String? = nil
250+
) : Nil
251+
end
252+
253+
# ===================================
254+
# Locatable Interface functions
255+
# ===================================
256+
def locate_user(email : String? = nil, username : String? = nil)
257+
logger.debug { "sensor incapable of locating #{email} or #{username}" }
258+
[] of Nil
259+
end
260+
261+
def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
262+
logger.debug { "sensor incapable of tracking #{email} or #{username}" }
263+
# we could find the floorsense user, grab the reservations the user has
264+
# and list them here, but probably not amazingly useful
265+
[] of String
266+
end
267+
268+
def check_ownership_of(mac_address : String) : OwnershipMAC?
269+
# "lb=#{@bank_id}&lk=#{locker.id}"
270+
return nil unless mac_address.starts_with?("lb=")
271+
floor_mac = URI::Params.parse mac_address
272+
locker_bank = floor_mac["lb"]
273+
locker_key = floor_mac["lk"]
274+
locker = locker_banks[locker_bank].locker_hash[locker_key]
275+
276+
has_reservation = false
277+
if user_id = locker.allocated_to
278+
if time = locker.allocated_until
279+
has_reservation = time > Time.utc
280+
else
281+
has_reservation = true
282+
end
283+
end
284+
285+
if has_reservation
286+
{
287+
location: "locker",
288+
assigned_to: staff_api.user(locker.allocated_to).get["email"].as_s,
289+
mac_address: mac_address,
290+
}
291+
end
292+
rescue
293+
nil
294+
end
295+
296+
def device_locations(zone_id : String, location : String? = nil)
297+
logger.debug { "searching lockers in zone #{zone_id}" }
298+
return [] of Nil if location && location != "locker"
299+
300+
building = building_id
301+
level_zone = zone_id == building ? nil : zone_id
302+
return [] of Nil if level_zone && !level_zone.in?(levels)
303+
304+
now = Time.utc
305+
locker_banks.values.flat_map do |bank|
306+
next [] of PlaceLocker if level_zone && bank.level_id != level_zone
307+
308+
bank.locker_hash.values.compact_map do |locker|
309+
if locker.allocated_to
310+
if time = locker.allocated_until
311+
PlaceLocker.new(bank.id, locker, building) if time > now
312+
else
313+
PlaceLocker.new(bank.id, locker, building)
314+
end
315+
end
316+
end
317+
end
318+
end
319+
end

drivers/place/demo/lockers_spec.cr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
require "placeos-driver/spec"
2+
3+
DriverSpecs.mock_driver "Place::Demo::Lockers" do
4+
end

0 commit comments

Comments
 (0)