Skip to content

Commit

Permalink
Merge pull request #22 from vapor-ware/mhink-lock-routes
Browse files Browse the repository at this point in the history
Add routes for RCI 3525 door locks.
  • Loading branch information
MatthewHink committed Dec 21, 2017
2 parents 39b6017 + dcce540 commit daccd60
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 1 deletion.
50 changes: 50 additions & 0 deletions synse/blueprints/main_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,56 @@ def led_control(rack_id, board_id, device_id, led_state=None, led_color=None, bl
return make_json_response(response.get_response_data())


@core.route(url('/lock/<rack_id>/<board_id>/<device_id>'), methods=['GET'])
@core.route(url('/lock/<rack_id>/<board_id>/<device_id>/<action>'), methods=['GET'])
def lock_control(rack_id, board_id, device_id, action=None):
""" Control or get the status of a lock.
Control operations:
lock
unlock
momentary_unlock
Args:
rack_id (str): the rack id of the lock.
board_id (str): the board id of the lock.
device_id (str): the device id of the lock.
action (str): The action to take on a lock.
Valid actions are None, status, lock, unlock, momentary_unlock.
None is the same as status.
Returns:
For status requests:
0 - Electrically unlocked and mechanically unlocked.
1 - Electrically unlocked and mechanically locked.
2 - Electrically locked and mechanically unlocked.
3 - Electrically locked and mechanically locked.
Raises:
Returns a 500 error if the lock command fails.
"""
board_id, device_id = check_valid_board_and_device(board_id, device_id)

# Validate action.
if not (
action is None or
action == 'status' or
action == 'lock' or
action == 'unlock' or
action == 'momentary_unlock'):
raise SynseException('Invalid action provided for lock control.')

cmd = current_app.config['CMD_FACTORY'].get_lock_command({
_s_.BOARD_ID: board_id,
_s_.DEVICE_ID: device_id,
_s_.DEVICE_TYPE: get_device_type_code(const.DEVICE_LOCK),
_s_.DEVICE_TYPE_STRING: const.DEVICE_LOCK,
_s_.RACK_ID: rack_id,
_s_.ACTION: action,
})

device = get_device_instance(board_id)
response = device.handle(cmd)

return make_json_response(response.get_response_data())


@core.route(url('/fan/<rack_id>/<board_id>/<device_id>'), methods=['GET'])
@core.route(url('/fan/<rack_id>/<board_id>/<device_id>/<fan_speed>'), methods=['GET'])
def fan_control(rack_id, board_id, device_id, fan_speed=None):
Expand Down
1 change: 1 addition & 0 deletions synse/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def get_board_type(board_id):
DEVICE_FAN_SPEED = intern('fan_speed')
DEVICE_HUMIDITY = intern('humidity')
DEVICE_LED = intern('led')
DEVICE_LOCK = intern('lock')
DEVICE_POWER = intern('power')
DEVICE_POWER_SUPPLY = intern('power_supply')
DEVICE_PRESSURE = intern('pressure')
Expand Down
9 changes: 9 additions & 0 deletions synse/devicebus/command_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ def get_led_command(self, data):
"""
return Command(CommandId.LED, data, self._get_next_sequence())

def get_lock_command(self, data):
""" Generate a Lock Command.
Args:
data (dict): any key-value data that makes up the command context.
Returns:
Command: the generated command for Lock.
"""
return Command(CommandId.LOCK, data, self._get_next_sequence())

def get_fan_command(self, data):
""" Generate a Fan Command.
Expand Down
4 changes: 3 additions & 1 deletion synse/devicebus/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class CommandId(object):
FAN = 0x0c
HOST_INFO = 0x0d
RETRY = 0x0e
LOCK = 0x0f

@classmethod
def get_command_name(cls, command_id):
Expand All @@ -69,5 +70,6 @@ def get_command_name(cls, command_id):
cls.LED: 'LED',
cls.FAN: 'Fan',
cls.HOST_INFO: 'Host Info',
cls.RETRY: 'Retry'
cls.RETRY: 'Retry',
cls.LOCK: 'Lock',
}.get(command_id, 'Unknown Command')
1 change: 1 addition & 0 deletions synse/devicebus/devices/i2c/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@
Max11610Thermistor)
from synse.devicebus.devices.i2c.pca9632_led import PCA9632Led
from synse.devicebus.devices.i2c.sdp610_pressure import SDP610Pressure
from synse.devicebus.devices.i2c.rci3525_lock import RCI3525Lock
233 changes: 233 additions & 0 deletions synse/devicebus/devices/i2c/rci3525_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#!/usr/bin/env python
""" Synse lock for RCI3525 door locks. I2C protocol.
\\//
\/apor IO
-------------------------------
Copyright (C) 2015-17 Vapor IO
This file is part of Synse.
Synse is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Synse is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Synse. If not, see <http://www.gnu.org/licenses/>.
"""

import logging
import sys

import lockfile

import synse.strings as _s_
from synse.devicebus.constants import CommandId as cid
from synse.devicebus.response import Response
from synse.errors import SynseException
from synse.protocols.i2c_common import i2c_common

from .i2c_device import I2CDevice


logger = logging.getLogger(__name__)


class RCI3525Lock(I2CDevice):
""" Device subclass for the RCI 3525 door lock.
"""
_instance_name = 'rci-3525'

def __init__(self, **kwargs):
super(RCI3525Lock, self).__init__(**kwargs)

logger.debug('RCI3525Lock kwargs: {}'.format(kwargs))

# Sensor specific commands.
self._command_map[cid.READ] = self._read
self._command_map[cid.LOCK] = self._lock_function

self._lock = lockfile.LockFile(self.serial_lock)

self.channel = int(kwargs['channel'], 16)

self.board_id = int(kwargs['board_offset']) + int(kwargs['board_id_range'][0])

self.board_record = dict()
self.board_record['board_id'] = format(self.board_id, '08x')
self.board_record['devices'] = [
{
'device_id': kwargs['device_id'],
'device_type': 'lock',
'device_info': kwargs.get('device_info', 'Door Lock')
}
]

if self.hardware_type != 'production':
raise ValueError(
'Only production hardware is supported initially. '
'We may add emulator support later.')

self.lock_number = int(kwargs['lock_number'])
if not 1 <= self.lock_number <= 12:
raise ValueError('Lock number {} out of range 1-12.'.format(self.lock_number))

logger.debug('RCI3525Lock self: {}'.format(dir(self)))

def _read(self, command):
""" Read the data off of a given board's device.
Args:
command (Command): the command issued by the Synse endpoint
containing the data and sequence for the request.
Returns:
Response: a Response object corresponding to the incoming Command
object, containing the data from the read response.
"""
# Get the command data out from the incoming command.
device_id = command.data[_s_.DEVICE_ID]
device_type_string = command.data[_s_.DEVICE_TYPE_STRING]

try:
# Validate device to ensure device id and type are ok.
self._get_device_by_id(device_id, device_type_string)

reading = self._read_sensor()

if reading is not None:
return Response(
command=command,
response_data=reading
)

# If we get here, there was no sensor device found, so we must raise.
logger.exception('No response for sensor reading for command: {}'.format(command.data))
raise SynseException('No sensor reading returned from I2C.')

except Exception:
logger.exception()
raise SynseException('Error reading lock (device id: {})'.format(
device_id)), None, sys.exc_info()[2]

def _read_sensor(self):
""" Convenience method to return the sensor reading.
If the sensor is configured to be read indirectly (e.g. from background)
it will do so -- otherwise, we perform a direct read.
"""
if self.from_background:
return self.indirect_sensor_read()
return self._direct_sensor_read()

def indirect_sensor_read(self):
"""Read the sensor data from the intermediary data file.
FIXME - reading from file is only for the POC. once we can
confirm that this works and have it stable for the short-term, we
will need to move on to the longer-term plan of having this done
via socket.
Returns:
dict: the thermistor reading value.
"""
logger.debug('indirect_sensor_read')
raise NotImplementedError('Indirect sensors reads are coming in the future.')
# Code below will either be used or deleted for indirect sensor reads.
# data_file = self._get_bg_read_file('{0:04x}'.format(self.lock_number))
# data = RCI3525Lock.read_sensor_data_file(data_file)
# return {'lock_status': data[0]}

def _direct_sensor_read(self):
""" Internal method for reading data off of the device.
Returns:
dict: Key is lock_status. Data are:
0 - Electrically unlocked and mechanically unlocked.
1 - Electrically unlocked and mechanically locked.
2 - Electrically locked and mechanically unlocked.
3 - Electrically locked and mechanically locked.
"""
with self._lock:
logger.debug('RCI3525Lock _direct_sensor_read: {}')
reading = i2c_common.lock_status(self.lock_number)
return {'lock_status': reading}

# Needs to be called something other than _lock since that is self._lock is
# a lockfile in subclasses of I2CDevice.
def _lock_function(self, command):
""" Read or write the lock state.
Args:
command (Command): the command issued by the Synse endpoint
containing the data and sequence for the request.
Returns:
Response: a Response object corresponding to the incoming Command
object, containing the data from the lock response.
"""
# Get the command data out from the incoming command.
device_id = command.data[_s_.DEVICE_ID]
device_type_string = command.data[_s_.DEVICE_TYPE_STRING]
action = command.data[_s_.ACTION]

try:
# Validate device to ensure device id and type are ok.
self._get_device_by_id(device_id, device_type_string)

if action is None or action == 'status':
reading = self._read_sensor()

elif action == 'lock':
i2c_common.lock_lock(self.lock_number)
reading = self._return_lock_status(action)

elif action == 'unlock':
i2c_common.lock_unlock(self.lock_number)
reading = self._return_lock_status(action)

elif action == 'momentary_unlock':
i2c_common.lock_momentary_unlock(self.lock_number)
reading = self._return_lock_status(action)

else:
raise SynseException('Invalid action provided for lock control.')

return Response(
command=command,
response_data=reading,
)

except Exception:
logger.exception('Error reading lock. Raising SynseException.')
raise SynseException('Error reading lock (device id: {})'.format(
device_id)), None, sys.exc_info()[2]

def _return_lock_status(self, action):
"""Get the lock status that we return on lock, unlock, and
momentary_unlock calls.
:param action: Action string from the caller URL.
:return: The lock status that we return on lock, unlock, and
momentary_unlock calls."""
# Make the call to get the current status. Electrical status is bit 1
# and mechanical status is bit 0.
reading = self._read_sensor()

# Update the electrical status based on the action parameter from the
# caller.
if action == 'lock':
# Set bit 1 to denote electrically locked.
reading['lock_status'] = reading['lock_status'] | 0x2
elif action == 'unlock' or action == 'momentary_unlock':
# Clear bit 1 to denote electrically unlocked.
reading['lock_status'] = reading['lock_status'] & 0xFD
else:
raise SynseException('Invalid action provided for lock control.')
return reading
2 changes: 2 additions & 0 deletions synse/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
# FAN
FAN_SPEED = intern('fan_speed')

# LOCK
ACTION = intern('action')

# ERRORS
ERR_BOARD_ID_NOT_REGISTERED = 'Board ID ({}) not registered with any known device bus.'

0 comments on commit daccd60

Please sign in to comment.