pax_global_header00006660000000000000000000000064144415645070014524gustar00rootroot0000000000000052 comment=41900badcc658dcd83a7cf02863cea6f4744b442 egctl-0.3/000077500000000000000000000000001444156450700124645ustar00rootroot00000000000000egctl-0.3/.gitignore000066400000000000000000000000301444156450700144450ustar00rootroot00000000000000*.o egctl tags cscope.* egctl-0.3/COPYING000066400000000000000000000020721444156450700135200ustar00rootroot00000000000000Copyright (c) 2014, 2017, 2023 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.3/Makefile000066400000000000000000000011011444156450700141150ustar00rootroot00000000000000# # Copyright (c) 2014 Vitaly Sinilin # # See the included COPYING file. # CFLAGS = -W -Wall -D_BSD_SOURCE -D_DEFAULT_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.3/NEWS000066400000000000000000000005621444156450700131660ustar00rootroot00000000000000egctl NEWS -- History of user-visible changes Version 0.3 Fixes ----- * Fix version number in the usage message * Fix warning when building with glibc >= 2.19 Version 0.2 New Features ------------ * HOME environment variable is taken into account when looking for the configuration file * Support for the EG-PMS-WLAN protocol * Empty passwords are allowed egctl-0.3/README000066400000000000000000000013421444156450700133440ustar00rootroot00000000000000egctl, Copyright (c) 2014, 2017, 2023 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/WLAN interface. It uses native data exchange protocol of the device, not HTTP. INSTALLATION make make PREFIX=/usr install The makefile also understands DESTDIR for a staged installation. SUPPORTED DEVICES Currently the following devices are supported: - EG-PMS-LAN - EG-PM2-LAN - EG-PMS2-LAN - EG-PMS-WLAN THANKS TO Mārtiņš Brīvnieks for testing with EG-PMS2-LAN. Philipp Kolmann for reporting about compatibility with EG-PM2-LAN. joanandk for investigation of the EG-PMS-WLAN protocol. Wolfram Sang for vigilance. egctl-0.3/egctl.1000066400000000000000000000051131444156450700136440ustar00rootroot00000000000000.\" .\" Copyright (c) 2014, 2017, 2023 Vitaly Sinilin .\" .\" See the included COPYING file. .\" .TH egctl 1 "2 Jan 2023" egctl .SH NAME egctl \- EnerGenie EG-PMS-LAN/WLAN 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/WLAN interface. It uses native data exchange protocol of the device, 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 ", " pms21 ", and " pmswlan. .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 Optional plain-text password. .SH SUPPORTED DEVICES .TS lB lB _ _ l l. Device Protocol EG-PMS-LAN pms20 EG-PM2-LAN pms21 EG-PMS2-LAN pms21 EG-PMS-WLAN pmswlan .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.3/egctl.c000066400000000000000000000337101444156450700137320ustar00rootroot00000000000000/* * egctl - EnerGenie EM-PMS-LAN control utility * * Copyright (c) 2014, 2017, 2023 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 #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 WLAN_STATE_ON 0x51 #define WLAN_STATE_OFF 0x92 #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, EG_PROTO_WLAN } 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; 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[PATH_MAX] = ""; char *home = getenv("HOME"); if (!home) { struct passwd *pwd = getpwuid(getuid()); if (pwd) home = pwd->pw_dir; } if (home) { snprintf(egtab, sizeof(egtab), "%s/.egtab", home); } 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 if (!strcmp(tok, "pmswlan")) proto = EG_PROTO_WLAN; 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; /* Key should be padded with trailing spaces. */ memset(key.octets, 0x20, KEY_LEN); char *tok = consume_until_whitespace(str); if (tok) { size_t keylen = strlen(tok); if (keylen > KEY_LEN) { warn("Password too long, only first %u chars " "will be considered", KEY_LEN); keylen = 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; const char *egtabs[] = { get_personal_egtab_name(), "/etc/egtab" }; for (i = 0; !ent_found && i < ARRAY_SIZE(egtabs); i++) { FILE *fp = fopen(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; } int wait_for_data_in_sock(int sock, struct timeval *timeout) { fd_set fds; FD_ZERO(&fds); FD_SET(sock, &fds); return (select(sock + 1, &fds, NULL, NULL, timeout) == 1); } void establish_connection(int sock) { int i; /* 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); if (wait_for_data_in_sock(sock, &tv)) return; } fatal("Unable to establish connection with device"); } Session authorize(int sock, Key key) { Session s; Res res; struct timeval tv = { 4, 0 }; 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. */ if (!wait_for_data_in_sock(sock, &tv)) 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; } uint8_t convert_wlan_state(uint8_t state) { switch (state) { case WLAN_STATE_ON: return STATE_ON; case WLAN_STATE_OFF: return STATE_OFF; } return STATE_INVALID; } Status convert_status(Status st, uint8_t (*convert_state_fn)(uint8_t)) { size_t i; for (i = 0; i < SOCKET_COUNT; i++) st.socket[i] = convert_state_fn(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 supported protocol versions (in * the subset of the protocol that we implement) is in state constants, * we just map all state constants to their equivalent ones from * protocol version 2.0. */ if (proto == EG_PROTO_V21) st = convert_status(st, convert_v21_state); else if (proto == EG_PROTO_WLAN) st = convert_status(st, convert_wlan_state); 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; } uint8_t get_toggle_ctrl(uint8_t state) { switch (state) { case STATE_ON: case STATE_ON_NO_VOLTAGE: return SWITCH_OFF; case STATE_OFF: case STATE_OFF_VOLTAGE: return SWITCH_ON; } return DONT_SWITCH; } 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: ctrl.socket[i] = get_toggle_ctrl(status.socket[i]); if (ctrl.socket[i] == DONT_SWITCH) warn("Cannot toggle socket %zu", i+1); 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.3: 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"); } 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.3/egtab000066400000000000000000000005511444156450700134720ustar00rootroot00000000000000# # /etc/egtab: egctl configuration file # # Name Protocol IP Port Password # --------- -------- --------------- ------- -------- # egpms pms20 192.168.0.10 5000 hackme # egpm2 pms21 192.168.0.11 5000 hackme # egpms2 pms21 192.168.0.12 5000 hackme # egpmswlan pmswlan 192.168.0.13 5000 hackme #