Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions pms_smartlock_base/models/lock_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ def _to_utc(dt):
"""Odoo stores naive UTC datetimes; the library requires UTC aware."""
return dt.replace(tzinfo=timezone.utc)

def _local_tz(self):
"""IANA timezone the vendor's hardware enforces schedules against — the
hotel's local timezone (one hotel, one timezone). Passed to the
connector on the context so user-centric vendors that store naive
wall-clock (Salto) can localize the UTC window; others ignore it."""
return self.reservation_id.pms_property_id.tz or self.room_id.pms_property_id.tz

@staticmethod
def _to_naive(dt):
"""Strip tzinfo from an aware UTC datetime for Odoo storage."""
Expand Down Expand Up @@ -268,16 +275,22 @@ def _grant_target_specs(self):
def _apply_grant(self, grant, specs=None):
"""Persist the credential from a vendor ``AccessGrant``. When ``specs``
is given (initial grant) the ``target_ids`` snapshot is rebuilt; on a
modify the targets are unchanged and only the PIN/ref/window move."""
modify the targets are unchanged and only the PIN/ref/window move.

A ``grant.pin`` of ``None`` means *unchanged*: the vendor kept the same
credential (and may be unable to read it back, e.g. Salto on a window
change), so we keep the stored PIN instead of overwriting it. An empty
string would be a real value and is persisted as such."""
self.ensure_one()
vals = {
"pin": grant.pin,
"vendor_grant_ref": grant.ref,
"date_from": self._to_naive(grant.starts_at),
"date_to": self._to_naive(grant.ends_at),
# A successful sync clears any prior permanent failure.
"failed": False,
}
if grant.pin is not None:
vals["pin"] = grant.pin
if specs is not None:
vals["target_ids"] = [(5, 0, 0)] + [(0, 0, spec) for spec in specs]
self.write(vals)
Expand Down Expand Up @@ -337,7 +350,13 @@ def _sync_create(self):
if not self._try_lock_gateways([s["lock_device_id"] for s in specs]):
self._raise_gateway_busy()
try:
connector = self.vendor_id.get_connector()
# Pass the reservation on the context so user-centric vendors (Salto)
# can name the credential after the guest. Passcode vendors ignore
# it. Only the create path needs it; modify/revoke don't.
connector = self.vendor_id.with_context(
smartlock_grant_reservation=self.reservation_id,
smartlock_local_tz=self._local_tz(),
).get_connector()
grant = connector.grant_access(
lock_ids=[s["lock_device_id"] for s in specs],
starts_at=self._to_utc(self.date_from),
Expand All @@ -359,7 +378,9 @@ def _sync_modify(self, date_from, date_to):
if not self._try_lock_gateways(self.target_ids.mapped("lock_device_id")):
self._raise_gateway_busy()
try:
connector = self.vendor_id.get_connector()
connector = self.vendor_id.with_context(
smartlock_local_tz=self._local_tz()
).get_connector()
grant = connector.modify_access(
grant_ref=self.vendor_grant_ref,
starts_at=self._to_utc(date_from),
Expand Down
105 changes: 105 additions & 0 deletions pms_smartlock_salto/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

===================
PMS Smartlock Salto
===================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:26c69df3ae9c193a848da9be5a495a7ac4d46f7514af84a563eb1029bd184435
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-commitsun%2Froomdoo--modules-lightgray.png?logo=github
:target: https://github.com/commitsun/roomdoo-modules/tree/16.0/pms_smartlock_salto
:alt: commitsun/roomdoo-modules

|badge1| |badge2| |badge3|

Salto KS smart lock provider for the PMS.

Adds the ``salto`` vendor type to *PMS Smartlock Base* and implements its
connector on top of the ``roomdoo_locks_salto`` library, so the PMS can grant,
revoke and reveal PIN-based access on Salto KS locks.

Salto KS is *user-centric*: a guest is a site user assigned to an access group
that links the locks for a validity window. Because Salto bills per subscribed
user, revoking a grant only **suspends** the user (frees the license and
disables the PIN); a retention cron hard-deletes the suspended user and its
access group 15 days after checkout, keeping the audit trail until then.

The guest's name (never the email, to avoid Salto emailing the guest) is set on
the site user and the reservation locator labels the access group, so the hotel
can tell whose credential is whose. The keypad confirm key defaults to the
Enter symbol ``↵``.

**Table of contents**

.. contents::
:local:

Configuration
=============

The Roomdoo app credentials identify Roomdoo's Salto KS integration (not the
hotel) and live in the deployment environment, never in the database. Set them
in ``.docker/*.env``:

* ``SALTO_CLIENT_ID`` and ``SALTO_CLIENT_SECRET``.

Then, on the lock vendor (**PMS ‣ Smart Locks ‣ Vendors**):

#. Set **Type** to *Salto KS*.
#. In the **Salto Credentials** tab, pick the **Salto Environment** and fill the
hotel's **Salto Username**, **Salto Password** and **Salto Site ID**.
#. Press **Fetch Salto roles** to pull the site's roles, then set **Guest Role**
to the basic *User* role (it only opens doors). Never assign an admin role to
guests; the integration account itself must be a site admin.

A misconfigured instance (missing environment variable) fails loudly instead of
authenticating with empty credentials.

The retention cron *Smartlocks: Purge revoked Salto grants (retention)* runs
daily and hard-deletes grants that were revoked more than 15 days ago.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/commitsun/roomdoo-modules/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/commitsun/roomdoo-modules/issues/new?body=module:%20pms_smartlock_salto%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Commitsun

Contributors
~~~~~~~~~~~~

* Commit [Sun] <info@commitsun.com>:

* Jesús V. Mayor <jesus@roomdoo.com>

Maintainers
~~~~~~~~~~~

This module is part of the `commitsun/roomdoo-modules <https://github.com/commitsun/roomdoo-modules/tree/16.0/pms_smartlock_salto>`_ project on GitHub.

You are welcome to contribute.
1 change: 1 addition & 0 deletions pms_smartlock_salto/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
24 changes: 24 additions & 0 deletions pms_smartlock_salto/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "PMS Smartlock Salto",
"version": "16.0.1.0.0",
"category": "Hotel/PMS",
"author": "Commitsun",
"license": "AGPL-3",
"website": "https://github.com/commitsun/roomdoo-smartlocks",
"summary": "Salto KS smartlock provider for the PMS",
"depends": [
"pms_smartlock_base",
],
"external_dependencies": {
"python": ["roomdoo_locks_salto"],
},
"data": [
"security/ir.model.access.csv",
"data/ir_cron.xml",
"views/lock_vendor_views.xml",
"views/lock_code_views.xml",
],
"installable": True,
"application": False,
"auto_install": False,
}
15 changes: 15 additions & 0 deletions pms_smartlock_salto/data/ir_cron.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record model="ir.cron" id="cron_purge_salto_grants">
<field name="name">Smartlocks: Purge revoked Salto grants (retention)</field>
<field name="model_id" ref="pms_smartlock_base.model_lock_code" />
<field name="state">code</field>
<field name="code">model._cron_purge_salto_grants()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="active" eval="True" />
</record>
</odoo>
Loading
Loading