Opened from the free fourth corner slot on the Alt key (switch_lights), closed by the full-width Keyboard key. One key per light, registry-driven labels, so the page renders whatever the lights-proxy reports rather than knowing any entity ids. ON is blue, OFF is pink, anything unreachable is stale grey, and the glyph flips between a lit lamp (0xE025) and an unlit twin (0xE026) so an unreachable light can never look lit. Toggles are sent once and never retried.
443 lines
20 KiB
Java
443 lines
20 KiB
Java
package juloo.keyboard2;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import org.junit.Test;
|
|
import static org.junit.Assert.assertEquals;
|
|
import static org.junit.Assert.assertFalse;
|
|
import static org.junit.Assert.assertNotNull;
|
|
import static org.junit.Assert.assertNull;
|
|
import static org.junit.Assert.assertTrue;
|
|
|
|
/** The lights page: the state parsing the page is driven by ([LightsProxy]), and the page itself
|
|
([LightsPage]). Android classes cannot be constructed in a plain unit test, so the drawing is
|
|
covered where it is pure - the colour mapping, the glyph choice, the label - and the theme
|
|
wiring is checked by reading res/values/themes.xml and the built APK instead. */
|
|
public class LightsProxyTest
|
|
{
|
|
// ------------------------------------------------------------------ parsing a /lights answer
|
|
|
|
@Test
|
|
public void theAnswerIsParsedIntoOneLightPerEntry()
|
|
{
|
|
LightsProxy.Light[] lights = LightsProxy.parseLights(
|
|
"{\"ok\": true, \"lights\": ["
|
|
+ "{\"id\": \"bedroom\", \"name\": \"Bedroom\", \"entity_id\": \"light.bedroom\","
|
|
+ " \"state\": \"on\", \"optimistic_state\": null, \"warning\": null},"
|
|
+ "{\"id\": \"stairs\", \"name\": \"Stairs\", \"entity_id\": \"light.stairs\","
|
|
+ " \"state\": \"off\", \"optimistic_state\": null, \"warning\": null}"
|
|
+ "]}");
|
|
assertNotNull(lights);
|
|
assertEquals(2, lights.length);
|
|
assertEquals("bedroom", lights[0].id);
|
|
assertEquals("Bedroom", lights[0].name);
|
|
assertEquals("light.bedroom", lights[0].entityId);
|
|
assertEquals(LightsProxy.State.ON, lights[0].state);
|
|
assertEquals(LightsProxy.State.OFF, lights[1].state);
|
|
assertNull("a read-back state needs no note", lights[0].note);
|
|
}
|
|
|
|
@Test
|
|
public void unavailableAndUnknownAreTheirOwnStatesAndNeverOff()
|
|
{
|
|
LightsProxy.Light[] lights = LightsProxy.parseLights(
|
|
"{\"lights\": ["
|
|
+ "{\"id\": \"a\", \"name\": \"A\", \"state\": \"unavailable\"},"
|
|
+ "{\"id\": \"b\", \"name\": \"B\", \"state\": \"unknown\"},"
|
|
+ "{\"id\": \"c\", \"name\": \"C\", \"state\": \" On \"},"
|
|
+ "{\"id\": \"d\", \"name\": \"D\", \"state\": \"nonsense\"},"
|
|
+ "{\"id\": \"e\", \"name\": \"E\"},"
|
|
+ "{\"id\": \"f\", \"name\": \"F\", \"state\": \"OFF\"}"
|
|
+ "]}");
|
|
assertNotNull(lights);
|
|
assertEquals(LightsProxy.State.UNKNOWN, lights[0].state);
|
|
assertEquals(LightsProxy.State.UNKNOWN, lights[1].state);
|
|
assertEquals(LightsProxy.State.ON, lights[2].state);
|
|
assertEquals(LightsProxy.State.UNKNOWN, lights[3].state);
|
|
assertEquals(LightsProxy.State.UNKNOWN, lights[4].state);
|
|
assertEquals(LightsProxy.State.OFF, lights[5].state);
|
|
for (LightsProxy.Light l : lights)
|
|
assertFalse("nothing unreadable may be reported as switched off",
|
|
l.state == LightsProxy.State.OFF && !l.id.equals("f"));
|
|
}
|
|
|
|
@Test
|
|
public void fieldsTheKeyDoesNotShowNeverBecomeItsLabel()
|
|
{
|
|
// A registry entry carries more than the page uses - when it last changed, a read age, and the
|
|
// service's error code. None of it may take the place of the light's name: while no token is in
|
|
// place every light is unavailable, and the page would otherwise say "no_token" twelve times
|
|
// instead of naming the lights. The stale colour already says nobody can see them.
|
|
LightsProxy.Light[] lights = LightsProxy.parseLights(
|
|
"{\"lights\": ["
|
|
+ "{\"id\": \"bedroom\", \"name\": \"Bedroom\", \"state\": \"unavailable\","
|
|
+ " \"last_changed\": \"2026-09-28T02:00:00Z\", \"age_s\": 42,"
|
|
+ " \"error\": {\"code\": \"no_token\", \"detail\": \"no ha token configured\"},"
|
|
+ " \"warning\": \"state could not be confirmed\"}"
|
|
+ "]}");
|
|
assertNotNull(lights);
|
|
assertEquals("Bedroom", lights[0].name);
|
|
assertNull("the answer to a read is not a note on the key", lights[0].note);
|
|
assertEquals("an unreachable light is the stale look, never off",
|
|
LightsProxy.State.UNKNOWN, lights[0].state);
|
|
}
|
|
|
|
@Test
|
|
public void anEntryForALightTheBoxCannotSeeKeepsItsName()
|
|
{
|
|
// A light whose entity is unreachable comes back with an error object instead of state.
|
|
LightsProxy.Light[] lights = LightsProxy.parseLights(
|
|
"{\"lights\": ["
|
|
+ "{\"id\": \"tv-room\", \"name\": \"TV room\", \"entity_id\": \"light.tv_room\","
|
|
+ " \"state\": \"unavailable\", \"error\": {\"code\": \"unreachable\", \"detail\": \"http\"}}"
|
|
+ "]}");
|
|
assertNotNull(lights);
|
|
assertEquals("TV room", lights[0].name);
|
|
assertEquals(LightsProxy.State.UNKNOWN, lights[0].state);
|
|
}
|
|
|
|
@Test
|
|
public void theAnswerIsReadWhateverTheLightWeightAndEscaping()
|
|
{
|
|
// Whitespace and escaped quotes inside a name must not derail the reader; the fields the page
|
|
// does not use (numbers, booleans, nested objects) are skipped harmlessly.
|
|
LightsProxy.Light[] lights = LightsProxy.parseLights(
|
|
"{ \"lights\" : [ {\n \"id\":\"a\",\n \"name\": \"Say \\\"hi\\\"\",\n \"brightness\": 128,\n"
|
|
+ " \"on\": true,\n \"attrs\": {\"a\": [1, 2, {\"b\": null}]},\n \"state\": \"on\" } ] }");
|
|
assertNotNull(lights);
|
|
assertEquals(1, lights.length);
|
|
assertEquals("Say \"hi\"", lights[0].name);
|
|
assertEquals(LightsProxy.State.ON, lights[0].state);
|
|
}
|
|
|
|
@Test
|
|
public void anEmptyWhitelistIsNoLightsNotNoAnswer()
|
|
{
|
|
LightsProxy.Light[] none = LightsProxy.parseLights("{\"lights\": []}");
|
|
assertNotNull("an empty list is an answer: the page has nothing to show", none);
|
|
assertEquals(0, none.length);
|
|
}
|
|
|
|
@Test
|
|
public void aBrokenOrUnexpectedAnswerIsNoDataNotZeroLights()
|
|
{
|
|
assertNull(LightsProxy.parseLights(null));
|
|
assertNull(LightsProxy.parseLights(""));
|
|
assertNull(LightsProxy.parseLights("<html>404</html>"));
|
|
assertNull(LightsProxy.parseLights("[1, 2, 3]"));
|
|
assertNull(LightsProxy.parseLights("{\"lights\": [{\"id\": \"a\""));
|
|
assertNull(LightsProxy.parseLights("{\"logs\": []}"));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ parsing a toggle answer
|
|
|
|
@Test
|
|
public void aToggleThatWasReadBackReportsTheStateTheBoxGave()
|
|
{
|
|
LightsProxy.ToggleResult r = LightsProxy.parseToggle(
|
|
"{\"ok\": true, \"id\": \"bedroom\", \"accepted\": true, \"state\": \"off\","
|
|
+ " \"confirmed\": true, \"optimistic_state\": \"off\", \"warning\": null}");
|
|
assertNotNull(r);
|
|
assertTrue(r.accepted);
|
|
assertTrue(r.confirmed);
|
|
assertEquals("the read-back, as Home Assistant gave it", "off", r.state);
|
|
assertEquals(LightsProxy.State.OFF, LightsProxy.stateOf(r.state));
|
|
assertNull("a read-back state needs no warning on the key", r.note());
|
|
}
|
|
|
|
@Test
|
|
public void aToggleTheBoxRefusedIsNotAState()
|
|
{
|
|
// The proxy refuses when Home Assistant cannot be reached, or when the token is missing - which
|
|
// is the state the endpoint is in until a token is put in place. The key must say why, and must
|
|
// NOT show the light as off.
|
|
LightsProxy.ToggleResult r = LightsProxy.parseToggle(
|
|
"{\"ok\": false, \"id\": \"bedroom\", \"accepted\": false, \"state\": null,"
|
|
+ " \"confirmed\": false, \"optimistic_state\": null, \"retried\": false,"
|
|
+ " \"error\": {\"code\": \"no_token\", \"detail\": \"no ha token configured\"}}");
|
|
assertNotNull(r);
|
|
assertFalse(r.accepted);
|
|
assertFalse(r.confirmed);
|
|
assertNull("a refused toggle says nothing about the state", r.state);
|
|
assertEquals("the service's own reason is what the key says", "no_token", r.note());
|
|
}
|
|
|
|
@Test
|
|
public void theRefusalOfAServiceIsTheReasonTheKeyShows()
|
|
{
|
|
// The refusal the running service actually gives, recorded from
|
|
// POST http://127.0.0.1:8772/lights/no-such-light/toggle (HTTP 404): a flat code and a prose
|
|
// reason, and no state at all - so the key must show the code, not a guess.
|
|
LightsProxy.ToggleResult r = LightsProxy.parseToggle(
|
|
"{\"ts\": \"2026-09-28T03:14:51Z\", \"error\": \"unknown id\", \"code\": \"unknown_id\","
|
|
+ " \"id\": \"no-such-light\", \"known_ids\": [\"bedroom\"], \"detail\": \"only ids listed"
|
|
+ " in lights.toml can be toggled\", \"accepted\": false, \"toggled\": false}");
|
|
assertNotNull(r);
|
|
assertFalse(r.accepted);
|
|
assertNull(r.state);
|
|
assertEquals("unknown_id", r.note());
|
|
}
|
|
|
|
@Test
|
|
public void aToggleNobodyCouldReadBackIsNotClaimedAsSuccess()
|
|
{
|
|
LightsProxy.ToggleResult r = LightsProxy.parseToggle(
|
|
"{\"ok\": true, \"id\": \"bedroom\", \"accepted\": true, \"state\": null,"
|
|
+ " \"confirmed\": false, \"optimistic_state\": \"on\","
|
|
+ " \"warning\": \"state could not be confirmed after 10s\"}");
|
|
assertNotNull(r);
|
|
assertTrue(r.accepted);
|
|
assertFalse("nothing was read back, so nothing is confirmed", r.confirmed);
|
|
assertNull("the prediction is not the state", r.state);
|
|
assertEquals("the prose is kept, but a key says the short form of it",
|
|
"state could not be confirmed after 10s", r.warning);
|
|
assertEquals("unconfirmed", r.note());
|
|
}
|
|
|
|
@Test
|
|
public void aPredictionIsNeverMistakenForAState()
|
|
{
|
|
// optimistic_state is what the proxy thinks the light will be; only `state` is read back.
|
|
LightsProxy.ToggleResult r = LightsProxy.parseToggle(
|
|
"{\"accepted\": true, \"id\": \"a\", \"optimistic_state\": \"on\", \"state\": null,"
|
|
+ " \"confirmed\": false}");
|
|
assertNotNull(r);
|
|
assertNull(r.state);
|
|
assertNotNull("an unconfirmed toggle must say something", r.note());
|
|
}
|
|
|
|
@Test
|
|
public void aBrokenToggleAnswerIsNoAnswer()
|
|
{
|
|
assertNull(LightsProxy.parseToggle(null));
|
|
assertNull(LightsProxy.parseToggle(""));
|
|
assertNull(LightsProxy.parseToggle("nonsense"));
|
|
assertNull("a body that says neither yes, no, nor what it saw", LightsProxy.parseToggle("{}"));
|
|
assertNull(LightsProxy.parseToggle("{\"id\": \"a\"}"));
|
|
assertNull(LightsProxy.parseToggle("{\"ok\": true, \"retried\": false}"));
|
|
assertNull(LightsProxy.parseToggle("[1, 2]"));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the colour rule
|
|
|
|
@Test
|
|
public void theColourRuleIsOnBlueOffPinkAndEverythingElseStale()
|
|
{
|
|
// The rule, all of it, in one place. The three colours come from the theme; these are the same
|
|
// values res/values/themes.xml carries (colorLightOn is the Roku arrows' #00E5FF).
|
|
final int blue = 0xFF00E5FF, pink = 0xFFFF2D95, grey = 0xFF8A8D93;
|
|
assertEquals(blue, LightsProxy.colorFor(LightsProxy.State.ON, blue, pink, grey));
|
|
assertEquals(pink, LightsProxy.colorFor(LightsProxy.State.OFF, blue, pink, grey));
|
|
assertEquals("an unreachable light is neither colour",
|
|
grey, LightsProxy.colorFor(LightsProxy.State.UNKNOWN, blue, pink, grey));
|
|
assertEquals("and neither is one nobody has heard about",
|
|
grey, LightsProxy.colorFor(null, blue, pink, grey));
|
|
}
|
|
|
|
@Test
|
|
public void theStateGlyphIsTheLitLampOnlyWhenTheLightIsOn()
|
|
{
|
|
assertEquals(0xE025, LightsPage.glyphFor(LightsProxy.State.ON).charAt(0));
|
|
assertEquals(0xE026, LightsPage.glyphFor(LightsProxy.State.OFF).charAt(0));
|
|
assertEquals("an unreachable light must not look lit",
|
|
0xE026, LightsPage.glyphFor(LightsProxy.State.UNKNOWN).charAt(0));
|
|
assertEquals(0xE026, LightsPage.glyphFor(null).charAt(0));
|
|
assertEquals(1, LightsPage.glyphFor(LightsProxy.State.ON).length());
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the labels
|
|
|
|
@Test
|
|
public void theLabelIsTheRegistryNameNotSomethingShorter()
|
|
{
|
|
// The short names the keyboard shows are the registry's own; there is no mapping from a light
|
|
// to a label on this side, so whatever the endpoint returns is what a key says.
|
|
assertEquals("Bedroom", LightsPage.displayName("Bedroom"));
|
|
assertEquals("TV room", LightsPage.displayName("TV room"));
|
|
assertEquals("Stairs", LightsPage.displayName("Stairs"));
|
|
assertEquals("space around a name is not part of it", "Bedroom",
|
|
LightsPage.displayName(" Bedroom\n"));
|
|
assertEquals("", LightsPage.displayName(null));
|
|
}
|
|
|
|
@Test
|
|
public void aNameTooLongForItsKeyIsCutVisibly()
|
|
{
|
|
assertEquals("Basement Stairs Light", LightsPage.ellipsize("Basement Stairs Light", 32));
|
|
String cut = LightsPage.displayName("Basement Stairs Light");
|
|
assertTrue("a cut label is marked as cut: " + cut, cut.endsWith("…"));
|
|
assertEquals(LightsPage.MAX_NAME_CHARS, cut.length());
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the page
|
|
|
|
@Test
|
|
public void oneKeyPerLightThreeToARow()
|
|
{
|
|
KeyboardData page = LightsPage.build(template(), lights(4));
|
|
assertEquals("3 lights, 1 stretched to the whole width, and the Keyboard key",
|
|
3, page.rows.size());
|
|
assertEquals(3, page.rows.get(0).keys.size());
|
|
assertEquals(1, page.rows.get(1).keys.size());
|
|
assertEquals(1, page.rows.get(2).keys.size());
|
|
assertEquals("a full row of three is the whole width", 9.9f, page.rows.get(0).keysWidth, 0.001f);
|
|
assertEquals(3.3f, page.rows.get(0).keys.get(0).width, 0.001f);
|
|
assertEquals("the short row fills the keyboard", 9.9f, page.rows.get(1).keysWidth, 0.001f);
|
|
assertEquals("one key stretched to the whole width", 9.9f, page.rows.get(1).keys.get(0).width,
|
|
0.001f);
|
|
assertEquals("the way back is the template's own key", 9.9f,
|
|
page.rows.get(2).keys.get(0).width, 0.001f);
|
|
assertEquals("Keyboard", page.rows.get(2).keys.get(0).keys[0].getString());
|
|
assertEquals(KeyValue.Event.SWITCH_TEXT, page.rows.get(2).keys.get(0).keys[0].getEvent());
|
|
}
|
|
|
|
@Test
|
|
public void everyKeyShowsItsLightNameAndItsStateGlyph()
|
|
{
|
|
KeyboardData page = LightsPage.build(template(), lights(3));
|
|
assertEquals("the name the registry gave", "Bedroom",
|
|
page.rows.get(0).keys.get(0).keys[0].getString());
|
|
assertEquals("TV room", page.rows.get(0).keys.get(2).keys[0].getString());
|
|
// The name and the glyph sit on the same key and press the same light; the glyph is the value
|
|
// drawn in the special font, which is what tells the two apart.
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
KeyboardData.Key k = page.rows.get(0).keys.get(i);
|
|
KeyValue name = k.keys[0], glyph = k.keys[LightsPage.GLYPH_SUB_INDEX];
|
|
assertTrue(LightsPage.isLightKey(name));
|
|
assertTrue(LightsPage.isStateGlyph(glyph));
|
|
assertFalse("the name is not the glyph", LightsPage.isStateGlyph(name));
|
|
assertEquals(LightsPage.eventFor(i + 1), name.getEvent());
|
|
assertEquals("both values press the same light", name.getEvent(), glyph.getEvent());
|
|
assertEquals(KeyValue.Kind.Event, name.getKind());
|
|
assertEquals("the stored glyph never claims a light is on",
|
|
(char)0xE026, glyph.getString().charAt(0));
|
|
assertEquals("the name is drawn in the ordinary font, not the glyph font",
|
|
false, name.hasFlagsAny(KeyValue.FLAG_KEY_FONT));
|
|
}
|
|
assertEquals(KeyValue.Event.LIGHT_3, page.rows.get(0).keys.get(2).keys[0].getEvent());
|
|
}
|
|
|
|
@Test
|
|
public void withNoAnswerYetThePlaceholdersStand()
|
|
{
|
|
KeyboardData page = LightsPage.build(template(), null);
|
|
assertEquals(2, page.rows.size());
|
|
assertEquals(3, page.rows.get(0).keys.size());
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
KeyboardData.Key k = page.rows.get(0).keys.get(i);
|
|
assertEquals(LightsPage.PLACEHOLDER, k.keys[0].getString());
|
|
assertTrue("a slot before the first answer still carries the unlit lamp",
|
|
LightsPage.isStateGlyph(k.keys[LightsPage.GLYPH_SUB_INDEX]));
|
|
}
|
|
// Nothing has been heard from the proxy, so nothing is ON and the page is stale.
|
|
assertTrue(LightsProxy.isStaleNow());
|
|
assertEquals(LightsProxy.State.UNKNOWN, LightsProxy.stateAt(1));
|
|
assertEquals(LightsPage.glyphFor(LightsProxy.State.UNKNOWN),
|
|
LightsPage.glyphFor(LightsProxy.stateAt(1)));
|
|
}
|
|
|
|
@Test
|
|
public void buildingThePageLeavesTheTemplateAlone()
|
|
{
|
|
KeyboardData tpl = template();
|
|
LightsPage.build(tpl, lights(5));
|
|
assertEquals("KeyboardData.load caches the template, so it must survive being built from",
|
|
2, tpl.rows.size());
|
|
assertEquals(3, tpl.rows.get(0).keys.size());
|
|
assertEquals("the template's slot still points at its own light", KeyValue.Event.LIGHT_1,
|
|
tpl.rows.get(0).keys.get(0).keys[0].getEvent());
|
|
assertEquals(LightsPage.PLACEHOLDER, tpl.rows.get(0).keys.get(0).keys[0].getString());
|
|
assertNull("and the template's corner is still empty",
|
|
tpl.rows.get(0).keys.get(0).keys[LightsPage.GLYPH_SUB_INDEX]);
|
|
}
|
|
|
|
@Test
|
|
public void moreLightsThanSlotsIsSaidRatherThanDropped()
|
|
{
|
|
LightsProxy.Light[] many = lights(LightsPage.MAX_LIGHTS + 1);
|
|
KeyboardData page = LightsPage.build(template(), many);
|
|
assertEquals(5, page.rows.size());
|
|
String last = page.rows.get(3).keys.get(2).keys[0].getString();
|
|
assertTrue("the last key must say how many lights it cannot show: " + last, last.endsWith("+1"));
|
|
assertTrue(last.length() <= LightsPage.MAX_NAME_CHARS);
|
|
}
|
|
|
|
@Test
|
|
public void aKeyThePageDoesNotOwnIsNotALightsKey()
|
|
{
|
|
KeyValue door = KeyValue.getSpecialKeyByName("switch_lights");
|
|
KeyValue back = KeyValue.getSpecialKeyByName("lights_close");
|
|
assertFalse(LightsPage.isLightKey(door));
|
|
assertFalse(LightsPage.isStateGlyph(door));
|
|
assertFalse(LightsPage.isLightKey(back));
|
|
assertFalse(LightsPage.isLightKey(null));
|
|
assertEquals(0, LightsPage.slotOf(null));
|
|
assertEquals(0, LightsPage.slotOf(back.getEvent()));
|
|
assertEquals((char)0xE025, door.getString().charAt(0));
|
|
assertTrue("the door is drawn in the special font", door.hasFlagsAny(KeyValue.FLAG_KEY_FONT));
|
|
assertEquals("and the way back is the same key as the Roku page's",
|
|
KeyValue.Event.SWITCH_TEXT, back.getEvent());
|
|
}
|
|
|
|
// ------------------------------------------------------------------ what a key says
|
|
|
|
@Test
|
|
public void aNoteSurvivesARefreshUntilTheBoxReportsAState()
|
|
{
|
|
LightsProxy.Light noted = new LightsProxy.Light("a", "A", "switch.a",
|
|
LightsProxy.State.UNKNOWN, "no_token", 1000L);
|
|
LightsProxy.Light[] previous = { noted };
|
|
// A refresh that still cannot read a state keeps the last answer ...
|
|
LightsProxy.Light stillUnknown = new LightsProxy.Light("a", "A", "switch.a",
|
|
LightsProxy.State.UNKNOWN, null, 0L);
|
|
assertEquals("no_token", LightsProxy.mergeNote(stillUnknown, previous).note);
|
|
// ... a real state from the box supersedes it ...
|
|
LightsProxy.Light known = new LightsProxy.Light("a", "A", "switch.a",
|
|
LightsProxy.State.ON, null, 0L);
|
|
assertNull(LightsProxy.mergeNote(known, previous).note);
|
|
// ... a light that is not in the previous list stands on its own ...
|
|
assertNull(LightsProxy.mergeNote(stillUnknown, new LightsProxy.Light[0]).note);
|
|
// ... and the sentence expires, so an old answer cannot pass for the current state.
|
|
assertNotNull(noted.noteOrNull(1000L + LightsProxy.NOTE_MS - 1));
|
|
assertNull(noted.noteOrNull(1000L + LightsProxy.NOTE_MS));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ helpers
|
|
|
|
/** The page skeleton, built by hand to the same shape as res/xml/lights.xml. */
|
|
private static KeyboardData template()
|
|
{
|
|
List<KeyboardData.Key> slots = new ArrayList<KeyboardData.Key>();
|
|
for (int i = 1; i <= LightsPage.SLOTS_PER_ROW; i++)
|
|
slots.add(key(KeyValue.getSpecialKeyByName("light_" + i), 3.3f, 0f));
|
|
List<KeyboardData.Key> close = new ArrayList<KeyboardData.Key>();
|
|
close.add(key(KeyValue.getSpecialKeyByName("lights_close"), 9.9f, 0f));
|
|
List<KeyboardData.Row> rows = new ArrayList<KeyboardData.Row>();
|
|
rows.add(new KeyboardData.Row(slots, 1.0f, 0f));
|
|
rows.add(new KeyboardData.Row(close, 1.0f, 0f));
|
|
return new KeyboardData(rows, 9.9f, null, null, null, "lights", false, false, false);
|
|
}
|
|
|
|
private static KeyboardData.Key key(KeyValue kv, float width, float shift)
|
|
{
|
|
KeyValue[] ks = new KeyValue[9];
|
|
ks[0] = kv;
|
|
return new KeyboardData.Key(ks, null, 0, width, shift, null, KeyboardData.Key.Role.Action);
|
|
}
|
|
|
|
/** [n] lights with short registry-style names, each row of three named in turn. */
|
|
private static LightsProxy.Light[] lights(int n)
|
|
{
|
|
String[] words = { "Bedroom", "Stairs", "TV room", "Light Four", "Light Five", "Light Six",
|
|
"Light Seven", "Light Eight", "Light Nine", "Light Ten", "Light Eleven", "Light Twelve",
|
|
"Light Thirteen" };
|
|
LightsProxy.Light[] out = new LightsProxy.Light[n];
|
|
for (int i = 0; i < n; i++)
|
|
out[i] = new LightsProxy.Light("id" + i, words[i], "switch." + i,
|
|
i % 2 == 0 ? LightsProxy.State.ON : LightsProxy.State.OFF, null, 0L);
|
|
return out;
|
|
}
|
|
}
|