225 lines
6.5 KiB
Objective-C
225 lines
6.5 KiB
Objective-C
#import <Cocoa/Cocoa.h>
|
|
#import <Carbon/Carbon.h>
|
|
#import <LuaSkin/LuaSkin.h>
|
|
#import <sys/socket.h>
|
|
#import <netinet/in.h>
|
|
#import <arpa/inet.h>
|
|
|
|
#define USERDATA_TAG "hs.milight"
|
|
|
|
typedef struct _bridge_t {
|
|
const char *ip;
|
|
int port;
|
|
int socket;
|
|
struct sockaddr_in sockaddr;
|
|
} bridge_t;
|
|
|
|
// Option value for SO_BROADCAST
|
|
int broadcastOption = 1;
|
|
|
|
#define cmd_suffix 0x55
|
|
|
|
static void pushCommand(lua_State *L, const char *cmd, int value) {
|
|
// t[cmd] = value
|
|
lua_pushinteger(L, value);
|
|
lua_setfield(L, -2, cmd);
|
|
}
|
|
|
|
int milight_cacheCommands(lua_State *L) {
|
|
lua_newtable(L);
|
|
|
|
pushCommand(L, "rgbw", 0x40);
|
|
pushCommand(L, "all_off", 0x41);
|
|
pushCommand(L, "all_on", 0x42);
|
|
pushCommand(L, "disco_slower", 0x43);
|
|
pushCommand(L, "disco_faster", 0x44);
|
|
pushCommand(L, "zone1_on", 0x45);
|
|
pushCommand(L, "zone1_off", 0x46);
|
|
pushCommand(L, "zone2_on", 0x47);
|
|
pushCommand(L, "zone2_off", 0x48);
|
|
pushCommand(L, "zone3_on", 0x49);
|
|
pushCommand(L, "zone3_off", 0x4A);
|
|
pushCommand(L, "zone4_on", 0x4B);
|
|
pushCommand(L, "zone4_off", 0x4C);
|
|
pushCommand(L, "disco", 0x4D);
|
|
pushCommand(L, "brightness", 0x4E);
|
|
pushCommand(L, "all_white", 0xC2);
|
|
pushCommand(L, "zone1_white", 0xC5);
|
|
pushCommand(L, "zone2_white", 0xC7);
|
|
pushCommand(L, "zone3_white", 0xC9);
|
|
pushCommand(L, "zone4_white", 0xCB);
|
|
|
|
// Convenience colors
|
|
pushCommand(L, "violet", 0x00);
|
|
pushCommand(L, "royalblue", 0x10);
|
|
pushCommand(L, "babyblue", 0x20);
|
|
pushCommand(L, "aqua", 0x30);
|
|
pushCommand(L, "mint", 0x40);
|
|
pushCommand(L, "seafoam", 0x50);
|
|
pushCommand(L, "green", 0x60);
|
|
pushCommand(L, "lime", 0x70);
|
|
pushCommand(L, "yellow", 0x80);
|
|
pushCommand(L, "yelloworange", 0x90);
|
|
pushCommand(L, "orange", 0xA0);
|
|
pushCommand(L, "red", 0xB0);
|
|
pushCommand(L, "pink", 0xC0);
|
|
pushCommand(L, "fuscia", 0xD0);
|
|
pushCommand(L, "lilac", 0xE0);
|
|
pushCommand(L, "lavendar", 0xF0);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/// hs.milight.new(ip[, port]) -> bridge
|
|
/// Constructor
|
|
/// Creates a new bridge object, which will be connected to the supplied IP address and port
|
|
///
|
|
/// Parameters:
|
|
/// * ip - A string containing the IP address of the MiLight WiFi bridge device. For convenience this can be the broadcast address of your network (e.g. 192.168.0.255)
|
|
/// * port - An optional number containing the UDP port to talk to the bridge on. Defaults to 8899
|
|
///
|
|
/// Returns:
|
|
/// * An `hs.milight` object
|
|
///
|
|
/// Notes:
|
|
/// * You can not use 255.255.255.255 as the IP address, to do so requires elevated privileges for the Hammerspoon process
|
|
static int milight_new(lua_State *L) {
|
|
const char *ip = luaL_checkstring(L, 1);
|
|
int port;
|
|
|
|
if (lua_isnone(L, 2)) {
|
|
port = 8899;
|
|
} else {
|
|
port = (int)luaL_checkinteger(L, 2);
|
|
}
|
|
|
|
bridge_t *bridge = lua_newuserdata(L, sizeof(bridge_t));
|
|
memset(bridge, 0, sizeof(bridge_t));
|
|
|
|
bridge->ip = ip;
|
|
bridge->port = port;
|
|
|
|
bridge->socket = socket(AF_INET, SOCK_DGRAM, 0);
|
|
if (strlen(bridge->ip) > 3) {
|
|
const char *last_three = &bridge->ip[strlen(bridge->ip)-3];
|
|
if (!strncmp(last_three, "255", 3)) {
|
|
setsockopt(bridge->socket, SOL_SOCKET, SO_BROADCAST, &broadcastOption, sizeof(broadcastOption));
|
|
}
|
|
}
|
|
|
|
bzero(&bridge->sockaddr, sizeof(bridge->sockaddr));
|
|
bridge->sockaddr.sin_family = AF_INET;
|
|
bridge->sockaddr.sin_addr.s_addr = inet_addr(bridge->ip);
|
|
bridge->sockaddr.sin_port = htons(bridge->port);
|
|
|
|
luaL_getmetatable(L, USERDATA_TAG);
|
|
lua_setmetatable(L, -2);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/// hs.milight:delete()
|
|
/// Method
|
|
/// Deletes an `hs.milight` object
|
|
///
|
|
/// Parameters:
|
|
/// * None
|
|
///
|
|
/// Returns:
|
|
/// * None
|
|
static int milight_del(lua_State *L) {
|
|
bridge_t *bridge = luaL_checkudata(L, 1, USERDATA_TAG);
|
|
|
|
close(bridge->socket);
|
|
|
|
bridge = nil;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// hs.milight:send(cmd[, value]) -> bool
|
|
/// Method
|
|
/// Sends a command to the bridge
|
|
///
|
|
/// Parameters:
|
|
/// * cmd - A command from the `hs.milight.cmd` table
|
|
/// * value - An optional value, if appropriate for the command (defaults to 0x00)
|
|
///
|
|
/// Returns:
|
|
/// * True if the command was sent, otherwise false
|
|
///
|
|
/// Notes:
|
|
/// * This is a low level command, you typically should use a specific method for the operation you want to perform
|
|
static int milight_send(lua_State *L) {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
bridge_t *bridge = luaL_checkudata(L, 1, USERDATA_TAG);
|
|
|
|
int cmd_key = (int)luaL_checkinteger(L, 2);
|
|
int value;
|
|
if (lua_isnone(L, 3)) {
|
|
value = 0x0;
|
|
} else {
|
|
value = (int)luaL_checkinteger(L, 3);
|
|
}
|
|
|
|
unsigned char cmd[3] = {cmd_key, value, cmd_suffix};
|
|
|
|
// NSLog(@"milight: sending '%x %x %x'(%i %i %i) to %s:%i", cmd[0], cmd[1], cmd[2], cmd[0], cmd[1], cmd[2], bridge->ip, bridge->port);
|
|
|
|
ssize_t result = sendto(bridge->socket, cmd, 3, 0, (struct sockaddr *)&bridge->sockaddr, sizeof(bridge->sockaddr));
|
|
|
|
if (result == 3) {
|
|
// NSLog(@"milight: sent.");
|
|
lua_pushboolean(L, true);
|
|
usleep(100000); // The bridge requires we sleep for 100ms after each command
|
|
} else if (result == -1) {
|
|
[skin logBreadcrumb:[NSString stringWithFormat:@"milight: Error sending command: %s", strerror(errno)]];
|
|
lua_pushboolean(L, false);
|
|
} else {
|
|
[skin logBreadcrumb:[NSString stringWithFormat:@"milight: Error, incorrect amount of data written (%lu bytes)", result]];
|
|
lua_pushboolean(L, false);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Lua/HS glue
|
|
static int milight_metagc(lua_State *L) {
|
|
milight_del(L);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int userdata_tostring(lua_State* L) {
|
|
bridge_t *bridge = luaL_checkudata(L, 1, USERDATA_TAG);
|
|
|
|
lua_pushstring(L, [[NSString stringWithFormat:@"%s: %s:%d (%p)", USERDATA_TAG, bridge->ip, bridge->port, lua_topointer(L, 1)] UTF8String]) ;
|
|
return 1 ;
|
|
}
|
|
|
|
static const luaL_Reg milightlib[] = {
|
|
{"_cacheCommands", milight_cacheCommands},
|
|
{"new", milight_new},
|
|
|
|
{NULL, NULL},
|
|
};
|
|
|
|
static const luaL_Reg milight_objectlib[] = {
|
|
{"delete", milight_del},
|
|
{"send", milight_send},
|
|
{"__tostring", userdata_tostring},
|
|
{"__gc", milight_metagc},
|
|
|
|
{NULL, NULL}
|
|
};
|
|
|
|
/* NOTE: The substring "hs_milight_internal" in the following function's name
|
|
must match the require-path of this file, i.e. "hs.milight.internal". */
|
|
|
|
int luaopen_hs_libmilight(lua_State *L) {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
[skin registerLibraryWithObject:USERDATA_TAG functions:milightlib metaFunctions:nil objectFunctions:milight_objectlib];
|
|
|
|
return 1;
|
|
}
|