api pin bindings

pull/716/head
gabrielburnworth 2018-03-13 17:34:30 -07:00
parent f7a40217c5
commit d4ffd61409
8 changed files with 143 additions and 48 deletions

View File

@ -6,7 +6,8 @@ import {
TaggedPlantPointer, TaggedGenericPointer, TaggedPeripheral, TaggedFbosConfig,
TaggedWebAppConfig,
TaggedSensor,
TaggedFirmwareConfig
TaggedFirmwareConfig,
TaggedPinBinding
} from "../../resources/tagged_resources";
import { ExecutableType } from "../../farm_designer/interfaces";
import { fakeResource } from "../fake_resource";
@ -123,6 +124,14 @@ export function fakeWebcamFeed(): TaggedWebcamFeed {
});
}
export function fakePinBinding(): TaggedPinBinding {
return fakeResource("PinBinding", {
id: idCounter++,
pin_num: 10,
sequence_id: 1
});
}
export function fakeSensor(): TaggedSensor {
return fakeResource("Sensor", {
id: idCounter++,
@ -131,6 +140,7 @@ export function fakeSensor(): TaggedSensor {
pin: 1
});
}
export function fakePeripheral(): TaggedPeripheral {
return fakeResource("Peripheral", {
id: ++idCounter,

View File

@ -129,7 +129,7 @@ export class API {
/** /api/device_configs/:id */
get deviceConfigPath() { return `${this.baseUrl}/api/device_configs`; }
/** /api/pin_bindings/:id */
get pinBindingPath() { return `${this.baseUrl}/api/pin_bindings`; }
get pinBindingPath() { return `${this.baseUrl}/api/pin_bindings/`; }
/** /api/farmware_installations/:id */
get farmwareInstallationPath() {
return `${this.baseUrl}/api/farmware_installations`;

View File

@ -213,6 +213,7 @@ export function urlFor(tag: ResourceName) {
Regimen: API.current.regimensPath,
Peripheral: API.current.peripheralsPath,
Sensor: API.current.sensorPath,
PinBinding: API.current.pinBindingPath,
Point: API.current.pointsPath,
User: API.current.usersPath,
Device: API.current.devicePath,

View File

@ -6,6 +6,11 @@ jest.mock("../../../device", () => ({
getDevice: () => (mockDevice)
}));
jest.mock("../../../api/crud", () => ({
destroy: jest.fn(),
initSave: jest.fn()
}));
import * as React from "react";
import { PinBindings, PinBindingsProps } from "../pin_bindings";
import { mount } from "enzyme";
@ -14,7 +19,10 @@ import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import { TaggedSequence } from "../../../resources/tagged_resources";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import {
fakeSequence, fakePinBinding
} from "../../../__test_support__/fake_state/resources";
import { destroy, initSave } from "../../../api/crud";
describe("<PinBindings/>", () => {
beforeEach(function () {
@ -37,12 +45,13 @@ describe("<PinBindings/>", () => {
dispatch: jest.fn(),
bot: bot,
resources: resources,
botToMqttStatus: "up"
botToMqttStatus: "up",
shouldDisplay: x => false,
};
}
it("renders", () => {
const wrapper = mount(<PinBindings {...fakeProps() } />);
const wrapper = mount(<PinBindings {...fakeProps()} />);
["pin bindings", "pin number", "none", "bind"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
["pi gpio 10", "sequence 1", "pi gpio 11", "sequence 2"].map(string =>
@ -52,39 +61,63 @@ describe("<PinBindings/>", () => {
expect(buttons.length).toBe(4);
});
it("unregisters pin", () => {
const dispatch = jest.fn();
it("unregisters pin: bot", () => {
const p = fakeProps();
p.dispatch = dispatch;
p.dispatch = jest.fn(x => x(jest.fn()));
const wrapper = mount(<PinBindings {...p} />);
const buttons = wrapper.find("button");
buttons.first().simulate("click");
dispatch.mock.calls[0][0](jest.fn());
expect(mockDevice.unregisterGpio).toHaveBeenCalledWith({
pin_number: 10
});
});
it("registers pin", () => {
const dispatch = jest.fn();
it("unregisters pin: api", () => {
const p = fakeProps();
p.dispatch = dispatch;
const s = fakeSequence();
s.body.id = 1;
p.resources = buildResourceIndex([fakePinBinding(), s]).index;
p.shouldDisplay = x => true;
const wrapper = mount(<PinBindings {...p} />);
const buttons = wrapper.find("button");
buttons.first().simulate("click");
expect(mockDevice.unregisterGpio).not.toHaveBeenCalled();
expect(destroy).toHaveBeenCalledWith(expect.stringContaining("PinBinding"));
});
it("registers pin: bot", () => {
const p = fakeProps();
p.dispatch = jest.fn(x => x(jest.fn()));
const wrapper = mount(<PinBindings {...p} />);
const buttons = wrapper.find("button");
expect(buttons.last().text()).toEqual("BIND");
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
buttons.last().simulate("click");
dispatch.mock.calls[0][0](jest.fn());
expect(mockDevice.registerGpio).toHaveBeenCalledWith({
pin_number: 1, sequence_id: 2
});
});
it("registers pin: api", () => {
const p = fakeProps();
p.dispatch = jest.fn();
p.shouldDisplay = x => true;
const wrapper = mount(<PinBindings {...p} />);
const buttons = wrapper.find("button");
expect(buttons.last().text()).toEqual("BIND");
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
buttons.last().simulate("click");
expect(mockDevice.registerGpio).not.toHaveBeenCalled();
expect(initSave).toHaveBeenCalledWith(expect.objectContaining({
body: { pin_num: 1, sequence_id: 2 }, kind: "PinBinding"
}));
});
it("sets sequence id", () => {
const p = fakeProps();
const s = p.resources.references[p.resources.byKind.Sequence[0]];
const id = s && s.body.id;
const wrapper = mount(<PinBindings {...p } />);
const wrapper = mount(<PinBindings {...p} />);
expect(wrapper.state().sequenceIdInput).toEqual(undefined);
// tslint:disable-next-line:no-any
const instance = wrapper.instance() as any;
@ -93,7 +126,7 @@ describe("<PinBindings/>", () => {
});
it("sets pin", () => {
const wrapper = mount(<PinBindings {...fakeProps() } />);
const wrapper = mount(<PinBindings {...fakeProps()} />);
expect(wrapper.state().pinNumberInput).toEqual(undefined);
// tslint:disable-next-line:no-any
const instance = wrapper.instance() as any;

View File

@ -8,9 +8,9 @@ import {
DropDownItem
} from "../../ui/index";
import { ToolTips } from "../../constants";
import { BotState } from "../interfaces";
import { BotState, ShouldDisplay, Feature } from "../interfaces";
import { registerGpioPin, unregisterGpioPin } from "../actions";
import { findSequenceById } from "../../resources/selectors";
import { findSequenceById, selectAllPinBindings } from "../../resources/selectors";
import { ResourceIndex } from "../../resources/interfaces";
import { MustBeOnline } from "../must_be_online";
import { Popover, Position } from "@blueprintjs/core";
@ -18,12 +18,15 @@ import { RpiGpioDiagram, gpio } from "./rpi_gpio_diagram";
import { error } from "farmbot-toastr";
import { NetworkState } from "../../connectivity/interfaces";
import { SequenceSelectBox } from "../../sequences/sequence_select_box";
import { initSave, destroy } from "../../api/crud";
import { TaggedPinBinding, SpecialStatus } from "../../resources/tagged_resources";
export interface PinBindingsProps {
bot: BotState;
dispatch: Function;
botToMqttStatus: NetworkState;
resources: ResourceIndex;
shouldDisplay: ShouldDisplay;
}
export interface PinBindingsState {
@ -49,6 +52,30 @@ export class PinBindings
};
}
get pinBindings(): {
pin_number: number, sequence_id: number, uuid?: string
}[] {
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
return selectAllPinBindings(this.props.resources)
.map(x => {
return {
pin_number: x.body.pin_num,
sequence_id: x.body.sequence_id,
uuid: x.uuid
};
});
} else {
const { gpio_registry } = this.props.bot.hardware;
return Object.entries(gpio_registry || {})
.map(([pin_number, sequence_id]) => {
return {
pin_number: parseInt(pin_number),
sequence_id: parseInt(sequence_id || "")
};
});
}
}
changeSelection = (input: DropDownItem) => {
this.setState({ sequenceIdInput: parseInt(input.value as string) });
}
@ -65,13 +92,28 @@ export class PinBindings
}
}
taggedPinBinding =
(pin_num: number, sequence_id: number): TaggedPinBinding => {
return {
uuid: "WILL_BE_CHANGED_BY_REDUCER",
specialStatus: SpecialStatus.SAVED,
kind: "PinBinding",
body: { pin_num, sequence_id }
};
}
bindPin = () => {
const { pinNumberInput, sequenceIdInput } = this.state;
if (pinNumberInput && sequenceIdInput) {
this.props.dispatch(registerGpioPin({
pin_number: pinNumberInput,
sequence_id: sequenceIdInput
}));
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
this.props.dispatch(initSave(
this.taggedPinBinding(pinNumberInput, sequenceIdInput)));
} else {
this.props.dispatch(registerGpioPin({
pin_number: pinNumberInput,
sequence_id: sequenceIdInput
}));
}
this.setState({
pinNumberInput: undefined,
sequenceIdInput: undefined
@ -79,37 +121,41 @@ export class PinBindings
}
}
deleteBinding = (pin: number, uuid?: string) => {
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
this.props.dispatch(destroy(uuid || ""));
} else {
this.props.dispatch(unregisterGpioPin(pin));
}
}
get boundPins(): number[] | undefined {
const { gpio_registry } = this.props.bot.hardware;
return gpio_registry && Object.keys(gpio_registry).map(x => parseInt(x));
return this.pinBindings.map(x => x.pin_number);
}
currentBindingsList = () => {
const { bot, dispatch, resources } = this.props;
const { gpio_registry } = bot.hardware;
const { resources } = this.props;
return <div className={"bindings-list"}>
{gpio_registry &&
Object.entries(gpio_registry)
.map(([pin_number, sequence_id]) => {
return <Row key={`pin_${pin_number}_binding`}>
<Col xs={ColumnWidth.pin}>
{`Pi GPIO ${pin_number}`}
</Col>
<Col xs={ColumnWidth.sequence}>
{sequence_id ? findSequenceById(
resources, parseInt(sequence_id)).body.name : ""}
</Col>
<Col xs={ColumnWidth.button}>
<button
className="fb-button red"
onClick={() => {
dispatch(unregisterGpioPin(parseInt(pin_number)));
}}>
<i className="fa fa-minus" />
</button>
</Col>
</Row>;
})}
{this.pinBindings
.map(x => {
const { pin_number, sequence_id } = x;
return <Row key={`pin_${pin_number}_binding`}>
<Col xs={ColumnWidth.pin}>
{`Pi GPIO ${pin_number}`}
</Col>
<Col xs={ColumnWidth.sequence}>
{sequence_id ? findSequenceById(
resources, sequence_id).body.name : ""}
</Col>
<Col xs={ColumnWidth.button}>
<button
className="fb-button red"
onClick={() => this.deleteBinding(pin_number, x.uuid)}>
<i className="fa fa-minus" />
</button>
</Col>
</Row>;
})}
</div>;
}

View File

@ -94,7 +94,8 @@ export class Devices extends React.Component<Props, {}> {
dispatch={this.props.dispatch}
bot={this.props.bot}
resources={this.props.resources}
botToMqttStatus={botToMqttStatus} />}
botToMqttStatus={botToMqttStatus}
shouldDisplay={this.props.shouldDisplay} />}
</Col>
</Row>
</Page>;

View File

@ -59,6 +59,7 @@ export enum Feature {
sensors = "sensors",
change_ownership = "change_ownership",
variables = "variables",
api_pin_bindings = "api_pin_bindings",
jest_feature = "jest_feature", // for tests
}
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */

View File

@ -18,6 +18,7 @@ import {
TaggedPeripheral,
TaggedWebAppConfig,
TaggedFirmwareConfig,
TaggedPinBinding,
} from "./tagged_resources";
import { sortResourcesById } from "../util";
import { error } from "farmbot-toastr";
@ -69,6 +70,8 @@ export const selectAllPeripherals =
export const selectAllPoints = (i: ResourceIndex) => findAll<TaggedPoint>(i, "Point");
export const selectAllRegimens = (i: ResourceIndex) => findAll<TaggedRegimen>(i, "Regimen");
export const selectAllSensors = (i: ResourceIndex) => findAll<TaggedSensor>(i, "Sensor");
export const selectAllPinBindings =
(i: ResourceIndex) => findAll<TaggedPinBinding>(i, "PinBinding");
export const selectAllSequences = (i: ResourceIndex) => findAll<TaggedSequence>(i, "Sequence");
export const selectAllTools = (i: ResourceIndex) => findAll<TaggedTool>(i, "Tool");
export const selectAllSavedSensors =