pax_global_header00006660000000000000000000000064123746722320014522gustar00rootroot0000000000000052 comment=12d39c5fb410c91150763fddf300f4ca29a83328 egctl-0.1/000077500000000000000000000000001237467223200124605ustar00rootroot00000000000000egctl-0.1/.gitignore000066400000000000000000000000301237467223200144410ustar00rootroot00000000000000*.o egctl tags cscope.* egctl-0.1/COPYING000066400000000000000000000020561237467223200135160ustar00rootroot00000000000000Copyright (c) 2014 Vitaly Sinilin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. egctl-0.1/Makefile000066400000000000000000000010571237467223200141230ustar00rootroot00000000000000# # Copyright (c) 2014 Vitaly Sinilin # # See the included COPYING file. # CFLAGS = -W -Wall -D_BSD_SOURCE ifdef DEBUG CFLAGS += -g -DDEBUG endif DESTDIR = PREFIX = /usr/local bindir = $(PREFIX)/bin mandir = $(PREFIX)/share/man override bindir := $(DESTDIR)$(bindir) override mandir := $(DESTDIR)$(mandir) all: egctl install: egctl install -D egctl $(bindir)/egctl install -D -m 644 egctl.1 $(mandir)/man1/egctl.1 uninstall: $(RM) $(bindir)/egctl $(RM) $(mandir)/man1/egctl.1 clean: $(RM) egctl .PHONY: all install uninstall clean egctl-0.1/README000066400000000000000000000007411237467223200133420ustar00rootroot00000000000000egctl, Copyright (c) 2014 Vitaly Sinilin Published under the terms of the MIT License. egctl is a program to control the state of EnerGenie Programmable surge protector with LAN interface. It uses native EG-PMS-LAN data exchange protocol version 2.0 or 2.1, not HTTP. INSTALLATION make make install SUPPORTED DEVICES Currently the following devices are supported: - EG-PMS-LAN - EG-PMS2-LAN THANKS TO Mārtiņš Brīvnieks for testing with EG-PMS2-LAN. egctl-0.1/egctl.1000066400000000000000000000050021237467223200136350ustar00rootroot00000000000000.\" .\" Copyright (c) 2014 Vitaly Sinilin .\" .\" See the included COPYING file. .\" .TH egctl 1 "19 August 2014" egctl .SH NAME egctl \- EnerGenie EG-PMS-LAN control utility .SH SYNOPSIS .B egctl .I NAME .RI [ "S1 S2 S3 S4" ] .SH DESCRIPTION .B egctl is a program to control the state of EnerGenie Programmable surge protector with LAN interface. It uses native EG-PMS-LAN data exchange protocol version 2.0 or 2.1, not HTTP. When executed with the only argument it dumps the state of the specified device. If all five arguments are specified, it changes the state of the device and dumps the new state. .br .SH OPTIONS .TP .I NAME The name of the device to control (as it is specified in the configuration file). This name has no relation to the IP address or the domain name. .TP .I Sn The action to perform on .IR n \-th socket. Possible values are: .BR on ", " off ", " toggle " and " left . .SH CONFIGURATION Configuration file is a table of devices. Each device is described on a separate line; fields on each line are separated by tabs or spaces. Lines starting with '#' are comments, blank lines are ignored. .TP .B The first field Name of device. It is a string that will be used to address the device in the utility commands. It is not necessary to be the same as .B Server name in the web interface. .TP .B The second field Protocol. Supported protocols are: .BR pms20 " and " pms21. .TP .B The third field IP address of device. .TP .B The fourth field TCP port of device. .RB ( "Power Manager client port" in the web interface). .TP .B The fifth field Password. .SH SUPPORTED DEVICES .TS lB lB _ _ l l. Device Protocol EG-PMS-LAN pms20 EG-PMS2-LAN pms21 .TE .SH EXAMPLES Suppose a user has two EG-PMS-LAN devices configured as follows: .IP 1. 192.168.0.10, port 5000, password hackme .br 2. 192.168.10.10, port 5001, password hackmesoftly .LP In order to control them using the utility the user needs to create a configuration file like the following: .IP eg1 pms20 192.168.0.10 5000 hackme .br eg2 pms20 192.168.10.10 5001 hackmesoftly .LP Now she can get the status of the devices with commands .IP .B egctl eg1 .br .B egctl eg2 .LP and switch the state of the AC power sockets with a command like .IP .B egctl eg1 on left left off .LP .SH FILES .TP .I ~/.egtab user's configuration file .TP .I /etc/egtab system-wide configuration file .SH BUGS This program cannot modify the internal schedule of the device. .SH AUTHOR Written by Vitaly Sinilin .SH TRADEMARKS EnerGenie is a registered trademark of Gembird Holding B.V. egctl-0.1/egctl.c000066400000000000000000000330231237467223200137230ustar00rootroot00000000000000/* * egctl - EnerGenie EM-PMS-LAN control utility * * Copyright (c) 2014 Vitaly Sinilin * * Published under the terms of the MIT License, * see the included COPYING file. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #define TASK_LEN 4 #define STATCRYP_LEN 4 #define CTRLCRYP_LEN 4 #define KEY_LEN 8 #define STATE_ON 0x11 #define STATE_ON_NO_VOLTAGE 0x12 #define STATE_OFF 0x22 #define STATE_OFF_VOLTAGE 0x21 #define STATE_INVALID 0xFF /* for internal use */ #define V21_STATE_ON 0x41 #define V21_STATE_OFF 0x82 #define SWITCH_ON 0x01 #define SWITCH_OFF 0x02 #define DONT_SWITCH 0x04 #define SOCKET_COUNT 4 /* AC power sockets, not network ones ;) */ #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) typedef enum { EG_PROTO_V20, EG_PROTO_V21 } Protocol; typedef enum { ACTION_ON, ACTION_OFF, ACTION_TOGGLE, ACTION_LEFT, ACTION_INVALID } Action; typedef struct { Action socket[SOCKET_COUNT]; } Actions; typedef struct { uint8_t octets[KEY_LEN]; } Key; typedef struct { /* since the protocol is little-endian, low word comes first */ uint16_t loword; uint16_t hiword; } __attribute__((__packed__)) Res; typedef struct { struct sockaddr_in addr; Protocol proto; Key key; } Config; typedef struct { uint8_t socket[SOCKET_COUNT]; } Status, Controls; typedef struct { uint8_t task[TASK_LEN]; Key key; } Session; const char *g_egtabs[] = { NULL, /* placeholder for ~/.egtab */ "/etc/egtab" }; void vfatal(const char *fmt, va_list ap) { vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); exit(EXIT_FAILURE); } void fatal(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfatal(fmt, ap); va_end(ap); } void warn(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } #ifdef DEBUG void dbg4(const char *name, const uint8_t *buf) { fprintf(stderr, "%8s: 0x%02X 0x%02X 0x%02X 0x%02X\n", name, buf[0], buf[1], buf[2], buf[3]); } #else #define dbg4(n,b) #endif void xread(int fd, void *buf, size_t count) { ssize_t ret = read(fd, buf, count); if (ret == (ssize_t)count) { return; } else if (ret == -1) { if (errno != EINTR) fatal("Unable to read from socket: %s", strerror(errno)); else ret = 0; } xread(fd, (char *)buf + ret, count - ret); } void xwrite(int fd, const void *buf, size_t count) { ssize_t ret = write(fd, buf, count); if (ret == (ssize_t)count) { return; } else if (ret == -1) { if (errno != EINTR) fatal("Unable to write to socket: %s", strerror(errno)); else ret = 0; } xwrite(fd, (char *)buf + ret, count - ret); } char *get_personal_egtab_name(void) { static char egtab[1024] = "/dev/null"; struct passwd *pwd = getpwuid(getuid()); if (pwd) { snprintf(egtab, sizeof(egtab), "%s/.egtab", pwd->pw_dir); } else { warn("Unable to determine user home directory"); } return egtab; } char *consume_until_whitespace(char **str) { char *tok = *str; if (tok) { /* strip leading whitespaces */ tok += strspn(tok, " \t"); if (*tok == '\0') { /* no tokens */ *str = NULL; tok = NULL; } else { char *eot = tok + strcspn(tok, " \t"); if (*eot == '\0') { /* last token */ *str = NULL; } else { *eot = '\0'; *str = eot + 1; } } } return tok; } Protocol consume_protocol(char **str) { Protocol proto; char *tok = consume_until_whitespace(str); if (!tok) fatal("Protocol isn't specified"); if (!strcmp(tok, "pms20")) proto = EG_PROTO_V20; else if (!strcmp(tok, "pms21")) proto = EG_PROTO_V21; else fatal("Unknown protocol %s", tok); return proto; } in_addr_t consume_ip_address(char **str) { in_addr_t addr; char *tok = consume_until_whitespace(str); if (!tok) fatal("IP address isn't specified"); addr = inet_addr(tok); if (addr == INADDR_NONE) { /* It is ok that INADDR_NONE screens 255.255.255.255, since * this address isn't appropriate here anyway. */ fatal("Invalid IP address specified"); } return addr; } in_port_t consume_tcp_port(char **str) { char *tok = consume_until_whitespace(str); if (!tok) fatal("TCP port isn't specified"); return htons(atoi(tok)); } Key consume_key(char **str) { Key key; size_t keylen; char *tok = consume_until_whitespace(str); if (!tok) fatal("Password isn't specified"); keylen = strlen(tok); if (keylen > KEY_LEN) { warn("Password too long, only first %u chars " "will be considered", KEY_LEN); keylen = KEY_LEN; } /* Key should be padded with trailing spaces */ memset(key.octets, 0x20, KEY_LEN); memcpy(key.octets, tok, keylen); return key; } int get_device_entry(const char *name, FILE *fp, Config *conf) { char buf[1024]; char *line; while ((line = fgets(buf, sizeof(buf), fp)) != NULL) { char *tabname; if (line[0] == '#') continue; line[strcspn(line, "\n")] = '\0'; tabname = consume_until_whitespace(&line); if (tabname && !strcmp(tabname, name)) { conf->proto = consume_protocol(&line); conf->addr.sin_addr.s_addr = consume_ip_address(&line); conf->addr.sin_port = consume_tcp_port(&line); conf->key = consume_key(&line); conf->addr.sin_family = AF_INET; return 1; } } return 0; } Config get_device_conf(const char *name) { Config conf; int opened_tabs = 0; int ent_found = 0; size_t i; for (i = 0; !ent_found && i < ARRAY_SIZE(g_egtabs); i++) { FILE *fp = fopen(g_egtabs[i], "r"); if (fp != NULL) { opened_tabs++; ent_found = get_device_entry(name, fp, &conf); fclose(fp); } } if (opened_tabs == 0) fatal("Unable to open any config file"); if (!ent_found) fatal("%s: unknown device", name); return conf; } int create_socket(const struct sockaddr_in *addr) { int ret; int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock == -1) fatal("Unable to create socket: %s", strerror(errno)); ret = connect(sock, (const struct sockaddr *)addr, sizeof(*addr)); if (ret != 0) fatal("Unable to connect: %s", strerror(errno)); return sock; } void establish_connection(int sock) { int i, ret; fd_set fds; /* When the device is still on timeout from a previous session * it doesn't respond to the first Start condition packet. So * we will take several attempts. */ for (i = 0; i < 4; i++) { struct timeval tv = { 0, 125000 }; xwrite(sock, "\x11", 1); FD_ZERO(&fds); FD_SET(sock, &fds); ret = select(sock + 1, &fds, NULL, NULL, &tv); if (ret == 1) return; } fatal("Unable to establish connection with device"); } Session authorize(int sock, Key key) { Session s; Res res; fd_set fds; struct timeval tv = { 4, 0 }; int ret; xread(sock, &s.task, sizeof(s.task)); dbg4("task", s.task); res.loword = ((s.task[0] ^ key.octets[2]) * key.octets[0]) ^ (key.octets[6] | (key.octets[4] << 8)) ^ s.task[2]; res.loword = htole16(res.loword); res.hiword = ((s.task[1] ^ key.octets[3]) * key.octets[1]) ^ (key.octets[7] | (key.octets[5] << 8)) ^ s.task[3]; res.hiword = htole16(res.hiword); dbg4("res", (uint8_t *)&res); xwrite(sock, &res, sizeof(res)); /* The protocol doesn't specify any explicit response on failed * authorization. So timeout is the only way to find out that * authorization hasn't been successful. */ FD_ZERO(&fds); FD_SET(sock, &fds); ret = select(sock + 1, &fds, NULL, NULL, &tv); if (ret != 1) fatal("Authorization failed"); s.key = key; return s; } Status decrypt_status(const uint8_t statcryp[], Session s) { Status st; size_t i; for (i = 0; i < SOCKET_COUNT; i++) st.socket[i] = (((statcryp[3-i] - s.key.octets[1]) ^ s.key.octets[0]) - s.task[3]) ^ s.task[2]; return st; } uint8_t convert_v21_state(uint8_t state) { switch (state) { case V21_STATE_ON: return STATE_ON; case V21_STATE_OFF: return STATE_OFF; } return STATE_INVALID; } Status convert_v21_status(Status st) { size_t i; for (i = 0; i < SOCKET_COUNT; i++) st.socket[i] = convert_v21_state(st.socket[i]); return st; } Status recv_status(int sock, Session s, Protocol proto) { Status st; uint8_t statcryp[STATCRYP_LEN]; xread(sock, &statcryp, sizeof(statcryp)); dbg4("statcryp", statcryp); st = decrypt_status(statcryp, s); /* Since the only difference between versions 2.0 and 2.1 of the * subset of the protocol that we use is the state constants, all * we need to do to support version 2.1 is just to map 2.1 * constants to the equivalent 2.0 ones. */ if (proto == EG_PROTO_V21) st = convert_v21_status(st); return st; } Action str_to_action(const char *action) { if (!strcmp(action, "on")) return ACTION_ON; else if (!strcmp(action, "off")) return ACTION_OFF; else if (!strcmp(action, "toggle")) return ACTION_TOGGLE; else if (!strcmp(action, "left")) return ACTION_LEFT; return ACTION_INVALID; } Actions argv_to_actions(char *argv[]) { Actions actions; size_t i; for (i = 0; i < SOCKET_COUNT; i++) { Action action = str_to_action(argv[i]); if (action == ACTION_INVALID) fatal("Invalid action for socket %zu: %s", i+1, argv[i]); actions.socket[i] = action; } return actions; } Controls construct_controls(Status status, Actions actions) { Controls ctrl; size_t i; for (i = 0; i < SOCKET_COUNT; i++) { switch (actions.socket[i]) { case ACTION_ON: ctrl.socket[i] = SWITCH_ON; break; case ACTION_OFF: ctrl.socket[i] = SWITCH_OFF; break; case ACTION_TOGGLE: switch (status.socket[i]) { case STATE_ON: case STATE_ON_NO_VOLTAGE: ctrl.socket[i] = SWITCH_OFF; break; case STATE_OFF: case STATE_OFF_VOLTAGE: ctrl.socket[i] = SWITCH_ON; break; default: warn("Cannot toggle socket %zu", i+1); ctrl.socket[i] = DONT_SWITCH; break; } break; default: case ACTION_LEFT: ctrl.socket[i] = DONT_SWITCH; } } return ctrl; } void send_controls(int sock, Session s, Controls ctrl) { size_t i; uint8_t ctrlcryp[CTRLCRYP_LEN]; /* Encrypt controls */ for (i = 0; i < SOCKET_COUNT; i++) ctrlcryp[i] = (((ctrl.socket[3-i] ^ s.task[2]) + s.task[3]) ^ s.key.octets[0]) + s.key.octets[1]; xwrite(sock, &ctrlcryp, sizeof(ctrlcryp)); } void close_session(int sock) { /* Empirically found way to close session w/o 4 second timeout on * the device side is to send some invalid sequence. This helps * to avoid a hiccup on subsequent run of the utility. */ xwrite(sock, "\x11", 1); } const char *get_state_str(uint8_t state) { switch (state) { case STATE_ON: return "on"; case STATE_ON_NO_VOLTAGE: return "on (no voltage!)"; case STATE_OFF: return "off"; case STATE_OFF_VOLTAGE: return "off (VOLTAGE IS PRESENT!)"; } return "unknown"; } void dump_status(Status st) { size_t i; for (i = 0; i < SOCKET_COUNT; i++) printf("socket %zu - %s\n", i+1, get_state_str(st.socket[i])); } int main(int argc, char *argv[]) { int sock; Config conf; Session sess; if (argc != 2 && argc != 6) { fatal("egctl 0.1: EnerGenie EG-PMS-LAN control utility\n\n" "Usage: egctl NAME [S1 S2 S3 S4]\n" " NAME is the name of the device in the egtab file\n" " Sn is an action to perform on n-th socket: " "on, off, toggle or left"); } g_egtabs[0] = get_personal_egtab_name(); conf = get_device_conf(argv[1]); sock = create_socket(&conf.addr); establish_connection(sock); sess = authorize(sock, conf.key); if (argc == 6) { Actions act = argv_to_actions(argv+2); Status status = recv_status(sock, sess, conf.proto); Controls ctrl = construct_controls(status, act); send_controls(sock, sess, ctrl); } dump_status(recv_status(sock, sess, conf.proto)); close_session(sock); close(sock); return EXIT_SUCCESS; } egctl-0.1/egtab000066400000000000000000000004011237467223200134600ustar00rootroot00000000000000# # /etc/egtab: egctl configuration file # # Name Protocol IP Port Password # --------- -------- --------------- ------- -------- # egpms pms20 192.168.0.10 5000 hackme # egpms2 pms21 192.168.0.11 5000 hackme #