Compare commits

..
6 Commits
Author SHA1 Message Date
cdo 138edf9c52 WIP 2023-07-14 15:50:45 +01:00
cdo 8fdc409a39 Create Python remote CLI 2023-05-21 20:53:57 +01:00
cdo cf11785f44 Write first version of TCP control protocol 2023-05-21 20:53:30 +01:00
cdo e961496973 Come up with TCP control protocol 2023-05-21 20:52:16 +01:00
cdo d2e2ea21df Fix inconsistent dayspec description 2023-05-21 20:45:54 +01:00
cdo 8c957d043a Move all firmware files to subdirectory 2023-05-21 15:06:44 +01:00
52 changed files with 850 additions and 2 deletions

No files matched your search

+81 -1
View File
@@ -8,7 +8,7 @@ Networked bedside clock utilising an ESP32-SOLO-1.
- [x] 7 segment display - [x] 7 segment display
- [x] WiFi networking - [x] WiFi networking
- [x] Console for configuration - [x] Console for configuration
- [ ] TCP API for configuration - [ ] TCP control protocol
- [x] SNTP synchronisation - [x] SNTP synchronisation
- [x] Snooze and dismiss buttons - [x] Snooze and dismiss buttons
- [x] Multiple alarms - [x] Multiple alarms
@@ -20,3 +20,83 @@ Networked bedside clock utilising an ESP32-SOLO-1.
The firmware is a standard ESP-IDF v5.0 project; refer to [Espressif's The firmware is a standard ESP-IDF v5.0 project; refer to [Espressif's
ESP-IDF ESP-IDF
docs](https://docs.espressif.com/projects/esp-idf/en/release-v5.0/esp32/index.html). docs](https://docs.espressif.com/projects/esp-idf/en/release-v5.0/esp32/index.html).
## TCP Control Protocol
Simple, binary, command / reponse protocol. Port number 3498.
Capabilities:
- Get firmware version
- Set time
- List, add and remove alarms
- List and modify settings
Clients should wait for a response before sending another command.
### Message Format
The command and response message formats are the same, except commands
have an instruction where responses have a status code:
| Offset (bytes) | Description | Length (bytes) |
|----------------|---------------------------|----------------|
| 0 | Instruction / status code | 1 |
| 1 | Parameter count | 1 |
| 2 | Parameters | *Variable* |
Parameters are structured as a sequence of type, value tuples:
| Offset | Description | Length (bytes) |
|--------|----------------|------------------------------|
| 0 | Type | 1 |
| 1 | Value | *Determined by length field* |
A maximum of 16 parameters is permitted.
### Instructions
| Instruction | Description | Command parameters | Response parameters |
|-------------|---------------|------------------------------|----------------------------------|
| 0 | Get version | None | Major (u8), Minor (u8) |
| 1 | Set time | UNIX Timestamp (u64) | None |
| 2 | List alarms | None | Sequence of ID (u8), Alarm pairs |
| 3 | Add alarm | Alarm (alarm) | Alarm ID (u8) |
| 4 | Remove alarm | Alarm ID (u8) | None |
| 5 | List settings | None | Sequence of key (string), value |
| 6 | Set setting | Key (string), value (string) | None |
### Status codes
| Code | Description | Parameters |
|------|-----------------|-------------------------|
| 0 | OK / Success | *Instruction-dependent* |
| 1 | Busy | None |
| 2 | Invalid command | None |
| 3 | Internal error | None |
| 4 | Out of memory | None |
### Types
| Code | Name | Length (bytes) | Structure |
|------|--------|----------------|------------------------------------------|
| 0 | U8 | 1 | |
| 1 | U16 | 2 | |
| 2 | U32 | 4 | |
| 3 | U64 | 8 | |
| 4 | String | - | Length (u8), UTF-8 |
| 5 | Alarm | 3 | Hour (u8), minute (u8), day bitmask (u8) |
All integers are big-endian.
#### Day Bitmask
| Bit (0 is least-significant) | Day |
|------------------------------|-----------|
| 0 | Monday |
| 1 | Tuesday |
| 2 | Wednesday |
| 3 | Thursday |
| 4 | Friday |
| 5 | Saturday |
| 6 | Sunday |
| 7 | *Unused* |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -264,7 +264,7 @@ void alarms_init(void)
"alarms", "alarms",
"List, add and remove alarms\n\nIf a dayspec is specified, it " "List, add and remove alarms\n\nIf a dayspec is specified, it "
"should be a sequence of 'x's and '-'s (for enabled or disabled " "should be a sequence of 'x's and '-'s (for enabled or disabled "
"respectively). For example: \"*-*-*--\" means Mondays, Wednesdays " "respectively). For example: \"x-x-x--\" means Mondays, Wednesdays "
"and Fridays only.", "and Fridays only.",
"alarms OR alarms add <hh:mm> [dayspec] OR alarms remove <index> OR " "alarms OR alarms add <hh:mm> [dayspec] OR alarms remove <index> OR "
"alarms <clear>", "alarms <clear>",
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "protocol_interface.c"
INCLUDE_DIRS "."
REQUIRES alarms
)
@@ -0,0 +1,244 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#include "protocol_interface.h"
#include "esp_log.h"
#include "lwip/sockets.h"
#include <string.h>
#define TAG "Protocol interface"
#define PORT 3498
static CommandHandler handlers[COMMAND_INSTRUCTION_COUNT];
static Message command;
static Message response;
static char string_buffer[CONFIG_MESSAGE_STRING_BUFFER_SIZE];
static char *string_buffer_free_ptr;
static bool receive(int socket, unsigned n, char *p)
{
int len;
do {
len = recv(socket, p, n, 0);
if (len < 0) {
ESP_LOGE(TAG, "Error reading from socket: %d", errno);
return false;
} else if (len == 0) {
ESP_LOGI(TAG, "Connection closed");
return false;
} else {
n -= len;
p += len;
}
} while (n > 0);
return true;
}
static bool receive_param(int socket, unsigned i)
{
MessageParam *param = &command.params[i];
uint8_t type_buf;
if (!receive(socket, 1, &type_buf))
return false;
// TODO handle invalid param type
param->type = (MessageParamType)type_buf;
uint8_t len, alarm_buf[3];
switch (param->type) {
case MESSAGE_PARAM_TYPE_U8:
if (!receive(socket, 1, (char *)&param->u8))
return false;
break;
case MESSAGE_PARAM_TYPE_U16:
if (!receive(socket, 2, (char *)&param->u16))
return false;
param->u16 = ntohs(param->u16);
break;
case MESSAGE_PARAM_TYPE_U32:
if (!receive(socket, 4, (char *)&param->u32))
return false;
param->u32 = ntohl(param->u32);
break;
case MESSAGE_PARAM_TYPE_U64:
if (!receive(socket, 8, (char *)&param->u64))
return false;
param->u64 = ntohll(param->u64);
break;
case MESSAGE_PARAM_TYPE_STRING:
if (!receive(socket, 1, &len))
return false;
char *buf = alloc_message_string(len);
// TODO handle out-of-memory
if (!receive(socket, len, buf))
return false;
param->string = buf;
break;
case MESSAGE_PARAM_TYPE_ALARM:
if (!receive(socket, 3, alarm_buf))
return false;
param->alarm.time.hour = alarm_buf[0];
param->alarm.time.minute = alarm_buf[1];
for (unsigned i = 0; i < WEEK_DAY_COUNT; ++i)
param->alarm.days[i] = (alarm_buf[2] >> i) & 1;
break;
}
}
static bool receive_command(int socket)
{
char header_buf[2];
if (!receive(socket, 2, header_buf))
return false;
// TODO handle invalid instruction
command.instruction = (CommandInstruction)header_buf[0];
command.param_count = (uint8_t)header_buf[1];
for (unsigned i = 0; i < command.param_count; ++i) {
if (!receive_param(socket, i))
return false;
}
return true;
}
static void send_response(int socket)
{
}
static void drop_message_strings(void)
{
string_buffer_free_ptr = string_buffer;
}
static void handle_command(void)
{
const CommandHandler handler = handlers[command.instruction];
if (handler == NULL) {
ESP_LOGW(
TAG, "Unhandled command, instruction: %u", command.instruction);
return;
}
const ResponseStatus status = handler(&command, &response);
if (status != RESPONSE_STATUS_OK) {
response.status = status;
response.param_count = 0;
}
send_response();
drop_message_strings();
}
void server_task(void *arg)
{
int status;
// Create listening socket
const int listen_socket = socket(AF_INET6, SOCK_STREAM, IPPROTO_IPV6);
if (listen_socket < 0) {
ESP_LOGE(TAG, "Error creating socket: %d", errno);
goto error_no_cleanup;
}
// Configure listening socket
int opt = 1;
setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// Bind socket to port (any address)
struct sockaddr_storage server_addr;
struct sockaddr_in6 *server_addr_ip6
= (struct sockaddr_in6 *)&server_addr;
memset(
&server_addr_ip6->sin6_addr.un, 0,
sizeof(server_addr_ip6->sin6_addr.un));
server_addr_ip6->sin6_family = AF_INET6;
server_addr_ip6->sin6_port = htons(PORT);
status = bind(
listen_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (status != 0) {
ESP_LOGE(TAG, "Error binding socket: %d", errno);
goto error_cleanup_socket;
}
// Listen on socket
status = listen(listen_socket, 1);
if (status != 0) {
ESP_LOGE(TAG, "Error listening on socket: %d", errno);
goto error_cleanup_socket;
}
while (1) {
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof(client_addr);
// Accept incoming connection
int socket = accept(
listen_socket, (struct sockaddr *)&client_addr, &addr_len);
if (socket < 0) {
ESP_LOGE(TAG, "Error accepting connection: %d", errno);
continue;
}
// Log client address, run command-response loop until
// connection closed.
const char *addr_str
= ((struct sockaddr_in6 *)&client_addr)->sin6_addr;
ESP_LOGI(TAG, "Accepted connection from %s", addr_str);
while (1) {
if (!receive_command())
break;
handle_command();
}
// Cleanup connection
shutdown(socket, 0);
close(socket);
}
error_cleanup_socket:
close(listen_socket);
error_no_cleanup:
vTaskDelete(NULL);
}
void protocol_interface_init(void)
{
memset(handlers, 0, sizeof(handlers));
string_buffer_free = string_buffer;
memset(command, 0, sizeof(command));
memset(response, 0, sizeof(response));
}
void set_command_handler(
CommandInstruction instruction, CommandHandler handler)
{
if (instruction < COMMAND_INSTRUCTION_COUNT) {
if (handlers[instruction] != NULL)
ESP_LOGW(TAG, "Handler #%u overwritten", instruction);
handlers[instruction] = handler;
} else {
ESP_LOGE(TAG, "Invalid instruction passed to %s()", __func__);
}
}
char *alloc_message_string(unsigned length)
{
if (string_buffer_free_ptr + length + 1
< string_buffer + CONFIG_MESSAGE_STRING_BUFFER_SIZE) {
char *s = string_buffer_free_ptr;
string_buffer_free_ptr += length + 1;
s[length] = '\0';
return s;
} else {
return NULL;
}
}
@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef PROTOCOL_INTERFACE_H
#define PROTOCOL_INTERFACE_H
#include "protocol_messages.h"
typedef ResponseStatus (*CommandHandler)(
const Message *command, Message *response_out);
/**
* Initialize the protocol interface and start listening for commands.
*/
void protocol_interface_init(void);
/**
* Set the handler for commands with a given intruction.
*
* The handler will be invoked when/if a command with the specified
* instruction is received. The response status should be returned,
* and upon success the response should be populated. Strings in the
* response must be allocated with alloc_message_string(); these will
* be freed by the interface when the response has been sent.
*
* Only a single handler can be set for a given instruction; if
* multiple are added then only the one added last will be used.
*/
void set_command_handler(
CommandInstruction instruction, CommandHandler handler);
/**
* Allocate a buffer for a string with the given length (in bytes),
* returning a pointer to it or NULL if space has ran out.
*
* The actual space allocated will be length + 1 bytes, and the last
* byte in this buffer will be set to the null character.
*/
char *alloc_message_string(unsigned length);
#endif
@@ -0,0 +1,69 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) Camden Dixie O'Brien
*/
#ifndef PROTOCOL_MESSAGES_H
#define PROTOCOL_MESSAGES_H
#include "alarm_types.h"
#include <stdint.h>
#define MESSAGE_MAX_PARAMS 16
typedef enum {
MESSAGE_PARAM_TYPE_U8 = 0,
MESSAGE_PARAM_TYPE_U16 = 1,
MESSAGE_PARAM_TYPE_U32 = 2,
MESSAGE_PARAM_TYPE_U64 = 3,
MESSAGE_PARAM_TYPE_STRING = 4,
MESSAGE_PARAM_TYPE_ALARM = 5,
MESSAGE_PARAM_TYPE_COUNT,
} MessageParamType;
typedef struct {
MessageParamType type;
union {
uint8_t u8;
uint16_t u16;
uint32_t u32;
uint64_t u64;
const char *string;
Alarm alarm;
};
} MessageParam;
typedef enum {
COMMAND_INSTRUCTION_GET_VERSION = 0,
COMMAND_INSTRUCTION_SET_TIME = 1,
COMMAND_INSTRUCTION_LIST_ALARMS = 2,
COMMAND_INSTRUCTION_ADD_ALARM = 3,
COMMAND_INSTRUCTION_REMOVE_ALARM = 4,
COMMAND_INSTRUCTION_LIST_SETTINGS = 5,
COMMAND_INSTRUCTION_SET_SETTING = 6,
COMMAND_INSTRUCTION_COUNT,
} CommandInstruction;
typedef enum {
RESPONSE_STATUS_OK = 0,
RESPONSE_STATUS_BUSY = 1,
RESPONSE_STATUS_INVALID_COMMAND = 2,
RESPONSE_STATUS_INTERNAL_ERROR = 3,
RESPONSE_STATUS_OUT_OF_SPACE = 4,
RESPONSE_STATUS_COUNT,
} ResponseStatus;
typedef struct {
union {
CommandInstruction instruction;
ResponseStatus status;
};
uint8_t param_count;
MessageParam params[MESSAGE_MAX_PARAMS];
} Message;
#endif
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+407
View File
@@ -0,0 +1,407 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) Camden Dixie O'Brien
import abc
import argparse
import enum
import socket
import struct
import sys
import time
class Weekday:
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
class Alarm:
SERIALIZED_LENGTH = 3
@staticmethod
def deserialize(alarm_bytes):
if len(alarm_bytes) != Alarm.SERIALIZED_LENGTH:
raise RuntimeError("Wrong number of bytes for alarm")
hour = alarm_bytes[0]
minute = alarm_bytes[1]
days = Alarm._read_day_bitmask(alarm_bytes[2])
return Alarm(hour, minute, days)
@staticmethod
def _read_day_bitmask(bitmask):
days = set()
for i in range(7):
if (bitmask >> i & 1) != 0:
days.add(Weekday(i))
return days
@staticmethod
def _make_day_bitmask(days):
bitmask = 0
for i in range(7):
if Weekday(i) in days:
bitmask |= 1 << i
return bitmask
def __init__(self, hour, minute, days):
self.hour = hour
self.minute = minute
self.days = days
def serialize(self):
day_bitmask = Alarm._make_day_bitmask(self.days)
return bytes([self.hour, self.minute, day_bitmask])
class Type(enum.Enum):
U8 = 0
U16 = 1
U32 = 2
U64 = 3
STRING = 4
ALARM = 5
@staticmethod
def length(type):
if type == Type.U8:
return 1
elif type == Type.U16:
return 2
elif type == Type.U32:
return 4
elif type == Type.U64:
return 8
elif type == Type.STRING:
return None
elif type == Type.ALARM:
return Alarm.SERIALIZED_LENGTH
else:
raise RuntimeError("Invalid param type")
class Param:
@staticmethod
def deserialize(self, type, value_bytes):
return Param(type, Param._deserialize_value(type, value_bytes))
@staticmethod
def _deserialize_value(self, type, value_bytes):
if type == Type.U8:
return value_bytes[0]
elif type == Type.U16:
return struct.unpack('!H', value_bytes)[0]
elif type == Type.U32:
return struct.pack('!I', value_bytes)[0]
elif type == Type.U64:
return struct.pack('!Q', value_bytes)[0]
elif type == Type.STRING:
# Strings handled separately due to variable length
raise NotImplemented
elif type == Type.ALARM:
return Alarm.deserialize(value_bytes)
else:
raise RuntimeError("Invalid param type")
def __init__(self, type, value):
self.type = type
self.value = value
def serialize(self):
if self.type == Type.U8:
return bytes([self.value])
elif self.type == Type.U16:
return struct.pack('!H', self.value)
elif self.type == Type.U32:
return struct.pack('!I', self.value)
elif self.type == Type.U64:
return struct.pack('!Q', self.value)
elif self.type == Type.STRING:
return bytes([len(self.value)]) + self.value.encode('utf-8')
elif self.type == Type.ALARM:
return self.value.serialize()
else:
raise RuntimeError("Invalid param type")
class Instruction(enum.Enum):
GET_VERSION = 0
SET_TIME = 1
LIST_ALARMS = 2
ADD_ALARM = 3
REMOVE_ALARM = 4
LIST_SETTINGS = 5
SET_SETTING = 6
class CommandFailed(Exception):
pass
class Command(abc.ABC):
def __init__(self, instruction, *params):
self.instruction = instruction
self.params = params
def serialize(self):
param_bytes = [param.serialize() for param in self.params]
return bytes([instruction]) + b''.join(param_bytes)
def run(self, connection):
response = connection.run(self)
if response.status != Status.OK:
raise CommandFailed()
return self.proc_response(response.params)
@abc.abstractmethod
def proc_response(self, params):
pass
class GetVersion(Command):
def __init__(self):
super().__init__(Instruction.GET_VERSION)
def proc_response(self, params):
return (params[0].value, params[1].value)
class SetTime(Command):
def __init__(self, timestamp):
super().__init__(Instruction.SET_TIME, Param(Type.U64, timestamp))
class ListAlarms(Command):
def __init__(self):
super().__init__(Instruction.LIST_ALARMS)
def proc_response(self, params):
pairs = zip(params[::2], params[1::2])
return {key.value: value.value for key, value in pairs}
class AddAlarm(Command):
def __init__(self, hour, minute, days):
alarm = Alarm(hour, minute, days)
super().__init__(Instruction.ADD_ALARM, Param(Type.ALARM, alarm))
def proc_response(self, params):
return params[0].value
class RemoveAlarm(Command):
def __init__(self, alarm_id):
super().__init__(Instruction.REMOVE_ALARM, Param(Type.U8, alarm_id))
class ListSettings(Command):
def __init__(self):
super().__init__(Instruction.LIST_SETTINGS)
def proc_response(self, params):
pairs = zip(params[::2], params[1::2])
return {key.value: value.value for key, value in pairs}
class SetSetting(Command):
def __init__(self, key, value):
key_param = Param(Type.STRING, key)
value_param = Param(Type.STRING, value)
super().__init__(Instruction.SET_SETTING, key_param, value_param)
class Status(enum.Enum):
OK = 0
BUSY = 1
INVALID_COMMAND = 2
INTERNAL_ERROR = 3
OUT_OF_SPACE = 4
@staticmethod
def description(status):
if status == OK:
return "OK"
elif status == BUSY:
return "Busy"
elif status == INVALID_COMMAND:
return "Invalid command"
elif status == INTERNAL_ERROR:
return "Internal error"
elif status == OUT_OF_SPACE:
return "Device out of space"
else:
raise RuntimeError("Invalid status")
class Response:
def __init__(self, status, params):
self.status = status
self.params = params
class Connection:
PORT = 3498
def __init__(self, host):
self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.socket.connect((host, self.PORT))
def run(self, command):
self._send(command)
return self._receive_response()
def close(self):
self.socket.close()
def _send(self, command):
command_bytes = command.serialize()
total_sent = 0
while total_sent < len(command_bytes):
sent = self.socket.send(command_bytes[total_sent:])
if sent == 0:
raise RuntimeError("Connection broken")
total_sent += sent
def _receive_response(self):
status, param_count = self._receive(2)
params = [self._receive_param() for _ in range(param_count)]
return Response(status, params)
def _receive_param(self):
type = self._receive(1)
if type == Type.STRING:
length = self._receive(1)
value = self._receive(length).decode('utf-8')
return Param(type, value)
else:
value_bytes = self._receive(Type.length(type))
return Param.deserialize(type, value_bytes)
def _receive(self, n):
chunks = []
remaining = n
while remaining > 0:
chunk = self.socket.recv(remaining)
if len(chunk) == 0:
raise RuntimeError("Connection broken")
remaining -= len(chunk)
chunks.append(chunk)
return b''.join(chunks)
def version_subcommand(connection, args):
major, minor = GetVersion().run(connection)
print(f"{major}.{minor}")
def sync_subcommand(connection, args):
timestamp = int(time.time())
SetTime(timestamp).run(connection)
def alarms_subcommand(connection, args):
def read_dayspec(dayspec):
days = set()
for i in range(7):
if dayspec[i] == 'x':
days.add(Weekday(i))
return days
def format_dayspec(days):
dayspec = ""
for i in range(7):
dayspec += 'x' if Weekday(i) in days else '-'
return dayspec
if args.action == None:
alarms = ListAlarms().run(connection)
for id, alarm in alarms.items():
dayspec = format_dayspec(alarm.days)
print(f"[{id:2}] {alarm.hour:02}:{alarm.minute:02} {dayspec}")
elif args.action == "add":
hour, minute = args.time.split(':')
days = read_dayspec(args.dayspec)
id = AddAlarm(int(hour), int(minute), days).run(connection)
print(f"{id}")
elif args.action == "rm":
RemoveAlarm(int(args.id)).run(connection)
def settings_subcommand(connection, args):
if args.action == None:
settings = ListSettings().run(connection)
for key, value in settings.items():
print(f"{key:15} {value}")
elif args.action == "set":
SetSetting(args.key, args.value).run(connection)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Control a bedside clock remotely")
parser.add_argument(
"--host", help="The hostname/IP of the clock",
default="bedside-clock")
subparsers = parser.add_subparsers(
help="Action to perform", dest="subcommand", required=True)
version_parser = subparsers.add_parser(
"version", help="Get the version of the clock",
description = "Get the version of the clock")
sync_parser = subparsers.add_parser(
"sync", help="Syncronise the clock to this machine",
description="Syncronise the clock to this machine")
alarms_parser = subparsers.add_parser(
"alarms", help="List and manage alarms",
description="List and manage alarms")
settings_parser = subparsers.add_parser(
"settings", help="List and manage settings",
description="List and manage settings")
alarms_subparsers = alarms_parser.add_subparsers(dest="action")
alarms_add_parser = alarms_subparsers.add_parser(
"add", help="Add an alarm", description="Add an alarm")
alarms_rm_parser = alarms_subparsers.add_parser(
"rm", help="Remove an alarm", description="Remove an alarm")
alarms_add_parser.add_argument(
"--workdays", const="xxxxx--", dest="dayspec", action="store_const")
alarms_add_parser.add_argument(
"--weekends", const="-----xx", dest="dayspec", action="store_const")
alarms_add_parser.add_argument(
"time", help="The time for the alarm (HH:MM)")
alarms_add_parser.add_argument(
"dayspec", help="""Days for the alarm to be active on.
Should be a sequence of 'x's and '-'s. For example,
\"x-x----\" means Mondays and Tuesdays only.""",
default="xxxxxxx")
alarms_rm_parser.add_argument(
"id", help="ID of the alarm to remove")
settings_subparsers = settings_parser.add_subparsers(dest="action")
settings_set_parser = settings_subparsers.add_parser(
"set", help="Set a setting", description="Set a setting")
settings_set_parser.add_argument(
"key", help="Key of the setting to set")
settings_set_parser.add_argument(
"value", help="Value of the setting to set")
args = parser.parse_args()
subcommand_map = {
"version": version_subcommand,
"sync": sync_subcommand,
"alarms": alarms_subcommand,
"settings": settings_subcommand,
}
connection = Connection(args.host)
subcommand_map[args.subcommand](connection, args)
connection.close()