Merge pull request #1439 from RickCarlino/interim_prod_branch

v8.0.5 - Iridescent Iris
pull/1443/head v8.0.5
Rick Carlino 2019-09-16 16:09:34 -05:00 committed by GitHub
commit 12eebc9f44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 237 additions and 106 deletions

View File

@ -6,15 +6,15 @@ module FarmEvents
has_executable_fields
BACKWARDS_END_TIME = "This event starts before it ends. Did you flip the "\
BACKWARDS_END_TIME = "This event starts before it ends. Did you flip the " \
"start and end times?"
BAD_START_TIME = "FarmEvent start time needs to be in the future, not" +
" the past."
BAD_START_TIME = "FarmEvent start time needs to be in the future, not" +
" the past."
required do
model :device, class: Device
model :device, class: Device
integer :repeat, min: 1
string :time_unit, in: FarmEvent::UNITS_OF_TIME
string :time_unit, in: FarmEvent::UNITS_OF_TIME
end
optional do
@ -29,12 +29,14 @@ module FarmEvents
end
def execute
FarmEvent.transaction do
p = inputs.merge(executable: executable)
# Needs to be set this way for cleanup operations:
p[:end_time] = (p[:start_time] + 1.minute) if is_one_time_event
p.delete(:body)
wrap_fragment_with(FarmEvent.create!(p))
FarmEvent.auto_sync_debounce do
FarmEvent.transaction do
p = inputs.merge(executable: executable)
# Needs to be set this way for cleanup operations:
p[:end_time] = (p[:start_time] + 1.minute) if is_one_time_event
p.delete(:body)
wrap_fragment_with(FarmEvent.create!(p))
end
end
rescue CeleryScript::TypeCheckError => q
add_error :farm_event, :farm_event, q.message

View File

@ -12,18 +12,13 @@ import {
readPing,
markStale,
markActive,
isInactive,
startPinging,
ACTIVE_THRESHOLD,
PING_INTERVAL
} from "../ping_mqtt";
import { Farmbot, RpcRequest, RpcRequestBodyItem } from "farmbot";
import { dispatchNetworkDown, dispatchNetworkUp } from "../index";
import { FarmBotInternalConfig } from "farmbot/dist/config";
const TOO_LATE_TIME_DIFF = ACTIVE_THRESHOLD + 1;
const ACCEPTABLE_TIME_DIFF = ACTIVE_THRESHOLD - 1;
const state: Partial<FarmBotInternalConfig> = {
LAST_PING_IN: 123,
LAST_PING_OUT: 456
@ -78,12 +73,6 @@ describe("ping util", () => {
expectActive();
});
it("checks if the bot isInactive()", () => {
expect(isInactive(1, 1 + TOO_LATE_TIME_DIFF)).toBeTruthy();
expect(isInactive(1, 1)).toBeFalsy();
expect(isInactive(1, 1 + ACCEPTABLE_TIME_DIFF)).toBeFalsy();
});
it("binds event handlers with startPinging()", (done) => {
const bot = fakeBot();
startPinging(bot);

View File

@ -47,9 +47,9 @@ describe("connectivity reducer", () => {
});
it("broadcasts PING_NO", () => {
pingNO("yep");
pingNO("yep", 123);
expect(store.dispatch).toHaveBeenCalledWith({
payload: { id: "yep", },
payload: { id: "yep", at: 123 },
type: "PING_NO"
});
});

View File

@ -51,7 +51,7 @@ export const pingOK = (id: string, at: number) => {
store.dispatch(action);
};
export const pingNO = (id: string) => {
const action = { type: Actions.PING_NO, payload: { id } };
export const pingNO = (id: string, at: number) => {
const action = { type: Actions.PING_NO, payload: { id, at } };
store.dispatch(action);
};

View File

@ -12,8 +12,7 @@ import { API } from "../api/index";
import { FarmBotInternalConfig } from "farmbot/dist/config";
import { now } from "../devices/connectivity/qos";
export const PING_INTERVAL = 1800;
export const ACTIVE_THRESHOLD = PING_INTERVAL * 3;
export const PING_INTERVAL = 2000;
export const LAST_IN: keyof FarmBotInternalConfig = "LAST_PING_IN";
export const LAST_OUT: keyof FarmBotInternalConfig = "LAST_PING_OUT";
@ -33,14 +32,10 @@ export function markActive() {
dispatchNetworkUp("bot.mqtt", now());
}
export function isInactive(last: number, now_: number): boolean {
return last ? (now_ - last) > ACTIVE_THRESHOLD : true;
}
export function sendOutboundPing(bot: Farmbot) {
const id = uuid();
const ok = () => pingOK(id, now()); markActive();
const no = () => pingNO(id); markStale();
const no = () => pingNO(id, now()); markStale();
dispatchQosStart(id);
bot.ping().then(ok, no);
}

View File

@ -14,8 +14,9 @@ export const DEFAULT_STATE: ConnectionState = {
"user.api": undefined
},
pings: {
}
},
};
type PingResultPayload = { id: string, at: number };
export let connectivityReducer =
generateReducer<ConnectionState>(DEFAULT_STATE)
@ -25,12 +26,12 @@ export let connectivityReducer =
pings: startPing(s.pings, payload.id)
};
})
.add<{ id: string, at: number }>(Actions.PING_OK, (s, { payload }) => {
.add<PingResultPayload>(Actions.PING_OK, (s, { payload }) => {
s.pings = completePing(s.pings, payload.id, payload.at);
return s;
})
.add<{ id: string }>(Actions.PING_NO, (s, { payload }) => {
.add<PingResultPayload>(Actions.PING_NO, (s, { payload }) => {
s.pings = failPing(s.pings, payload.id);
return s;

View File

@ -147,7 +147,7 @@ describe("sync()", function () {
describe("execSequence()", function () {
it("calls execSequence", async () => {
await actions.execSequence(1);
expect(mockDevice.execSequence).toHaveBeenCalledWith(1);
expect(mockDevice.execSequence).toHaveBeenCalledWith(1, undefined);
expect(success).toHaveBeenCalled();
});

View File

@ -151,9 +151,15 @@ export function execSequence(
const noun = t("Sequence execution");
if (sequenceId) {
commandOK(noun)();
return bodyVariables
? getDevice().execSequence(sequenceId, bodyVariables).catch(commandErr(noun))
: getDevice().execSequence(sequenceId).catch(commandErr(noun));
return getDevice()
.execSequence(sequenceId, bodyVariables)
.catch((x: Error) => {
if (x && (typeof x == "object") && (typeof x.message == "string")) {
error(x.message);
} else {
commandErr(noun);
}
});
} else {
throw new Error(t("Can't execute unsaved sequences"));
}

View File

@ -104,9 +104,9 @@ export const calculateLatency =
total: latency.length
};
/** SIDE EFFECT WARNING: We do analytics on every 100th ping to gauge
/** SIDE EFFECT WARNING: We do analytics on every nth ping to gauge
* overall system health. This is the least invasive place to put it. */
const doReport = !!report.total && !(report.total % 10);
const doReport = !!report.total && !(report.total % 100);
doReport && window.logStore.log("FBOS Ping QoS Message", report, "info");
return report;
};

View File

@ -12,26 +12,95 @@ jest.mock("../../../farmware/weed_detector/actions", () => ({
import * as React from "react";
import { mount, shallow } from "enzyme";
import { CreatePoints, CreatePointsProps } from "../create_points";
import {
CreatePoints,
CreatePointsProps,
mapStateToProps
} from "../create_points";
import { initSave } from "../../../api/crud";
import { deletePoints } from "../../../farmware/weed_detector/actions";
import { Actions } from "../../../constants";
import { clickButton } from "../../../__test_support__/helpers";
import { fakeState } from "../../../__test_support__/fake_state";
import { DeepPartial } from "redux";
import { CurrentPointPayl } from "../../interfaces";
const FAKE_POINT: CurrentPointPayl =
({ name: "My Point", cx: 13, cy: 22, r: 345, color: "red" });
describe("mapStateToProps", () => {
it("maps state to props", () => {
const state = fakeState();
state
.resources
.consumers
.farm_designer
.currentPoint = FAKE_POINT;
const result = mapStateToProps(state);
const { currentPoint } = result;
expect(currentPoint).toBeTruthy();
if (currentPoint) {
expect(currentPoint.cx).toEqual(13);
expect(currentPoint.cy).toEqual(22);
} else {
fail("Nope");
}
});
});
describe("<CreatePoints />", () => {
const fakeProps = (): CreatePointsProps => {
return {
dispatch: jest.fn(),
currentPoint: undefined
currentPoint: undefined,
deviceY: 1.23,
deviceX: 3.21
};
};
const fakeInstance = () => {
const props = fakeProps();
props.currentPoint = FAKE_POINT;
return new CreatePoints(props);
};
it("renders", () => {
const wrapper = mount(<CreatePoints {...fakeProps()} />);
["create point", "cancel", "delete", "x", "y", "radius", "color"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
});
it("updates specific fields", () => {
const i = fakeInstance();
const updater = i.updateValue("color");
type Event = React.SyntheticEvent<HTMLInputElement>;
const e: DeepPartial<Event> = {
currentTarget: {
value: "cheerful hue"
}
};
updater(e as Event);
expect(i.props.currentPoint).toBeTruthy();
expect(i.props.dispatch).toHaveBeenCalledWith({
payload: {
color: "cheerful hue",
cx: 13,
cy: 22,
name: "My Point",
r: 345,
},
type: "SET_CURRENT_POINT_DATA",
});
});
it("updates current point", () => {
const p = fakeInstance();
p.updateCurrentPoint();
expect(p.props.dispatch).toHaveBeenCalledWith({
type: "SET_CURRENT_POINT_DATA",
payload: { cx: 13, cy: 22, name: "My Point", r: 345, color: "red" },
});
});
it("creates point", () => {
const wrapper = mount(<CreatePoints {...fakeProps()} />);
wrapper.setState({ cx: 10, cy: 20, r: 30, color: "red" });
@ -93,17 +162,14 @@ describe("<CreatePoints />", () => {
const p = fakeProps();
p.currentPoint = { cx: 1, cy: 2, r: 3, color: "blue" };
const wrapper = shallow<CreatePoints>(<CreatePoints {...p} />);
wrapper.instance().getPointData();
expect(wrapper.instance().state).toEqual({
color: "blue", cx: 1, cy: 2, r: 3
});
});
it("fills the state with default data", () => {
const wrapper = shallow<CreatePoints>(<CreatePoints {...fakeProps()} />);
wrapper.instance().getPointData();
expect(wrapper.instance().state).toEqual({
color: "green", cx: 0, cy: 0, r: 1, name: "Created Point"
const i = wrapper.instance();
expect(i.state).toEqual({});
expect(i.getPointData()).toEqual({
name: undefined,
cx: 1,
cy: 2,
r: 3,
color: "blue"
});
});

View File

@ -6,12 +6,19 @@ jest.mock("../../../history", () => ({
history: { push: jest.fn() }
}));
const mockMoveAbs = jest.fn();
jest.mock("../../../device", () => {
return { getDevice: () => ({ moveAbsolute: mockMoveAbs }) };
});
import * as React from "react";
import { mount } from "enzyme";
import { EditPoint, EditPointProps, mapStateToProps } from "../point_info";
import { EditPoint, EditPointProps, mapStateToProps, moveToPoint } from "../point_info";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { fakeState } from "../../../__test_support__/fake_state";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { getDevice } from "../../../device";
describe("<EditPoint />", () => {
const fakeProps = (): EditPointProps => ({
@ -42,3 +49,12 @@ describe("mapStateToProps()", () => {
expect(props.findPoint(1)).toEqual(point);
});
});
describe("moveToPoint()", () => {
it("moves the device to a particular point", () => {
const coords = { x: 1, y: -2, z: 3 };
const mover = moveToPoint(coords);
mover();
expect(getDevice().moveAbsolute).toHaveBeenCalledWith(coords);
});
});

View File

@ -1,40 +1,54 @@
import * as React from "react";
import { connect } from "react-redux";
import { Everything, ResourceColor } from "../../interfaces";
import {
Everything,
ResourceColor
} from "../../interfaces";
import { initSave } from "../../api/crud";
import {
Row, Col, BlurableInput, ColorPicker
Row,
Col,
BlurableInput,
ColorPicker
} from "../../ui/index";
import { CurrentPointPayl } from "../interfaces";
import { Actions, Content } from "../../constants";
import { deletePoints } from "../../farmware/weed_detector/actions";
import { clone } from "lodash";
import { GenericPointer } from "farmbot/dist/resources/api_resources";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
DesignerPanel,
DesignerPanelHeader,
DesignerPanelContent
} from "./designer_panel";
import { parseIntInput } from "../../util";
import { t } from "../../i18next_wrapper";
export function mapStateToProps(props: Everything) {
export function mapStateToProps(props: Everything): CreatePointsProps {
const { position } = props.bot.hardware.location_data;
return {
dispatch: props.dispatch,
currentPoint: props.resources.consumers.farm_designer.currentPoint
currentPoint: props.resources.consumers.farm_designer.currentPoint,
deviceX: position.x || 0,
deviceY: position.y || 0,
};
}
export interface CreatePointsProps {
dispatch: Function;
currentPoint: CurrentPointPayl | undefined;
deviceX: number;
deviceY: number;
}
interface CreatePointsState {
name: string;
cx: number;
cy: number;
r: number;
color: string;
}
type CreatePointsState = Partial<CurrentPointPayl>;
const DEFAULTS: CurrentPointPayl = {
name: "Created Point",
cx: 1,
cy: 1,
r: 15,
color: "red"
};
@connect(mapStateToProps)
export class CreatePoints
@ -44,19 +58,26 @@ export class CreatePoints
this.state = {};
}
UNSAFE_componentWillReceiveProps() {
this.getPointData();
}
attr = <T extends (keyof CurrentPointPayl & keyof CreatePointsState)>(key: T,
fallback = DEFAULTS[key]): CurrentPointPayl[T] => {
const p = this.props.currentPoint;
const userValue = this.state[key] as CurrentPointPayl[T] | undefined;
const propValue = p ? p[key] : fallback;
if (typeof userValue === "undefined") {
return propValue;
} else {
return userValue;
}
};
getPointData = () => {
const point = this.props.currentPoint;
this.setState({
name: point ? point.name : "Created Point",
cx: point ? point.cx : 0,
cy: point ? point.cy : 0,
r: point ? point.r : 1,
color: point ? point.color : "green"
});
getPointData = (): CurrentPointPayl => {
return {
name: this.attr("name"),
cx: this.attr("cx"),
cy: this.attr("cy"),
r: this.attr("r"),
color: this.attr("color"),
};
}
cancel = () => {
@ -65,7 +86,10 @@ export class CreatePoints
payload: undefined
});
this.setState({
cx: undefined, cy: undefined, r: undefined, color: undefined
cx: undefined,
cy: undefined,
r: undefined,
color: undefined
});
}
@ -76,7 +100,7 @@ export class CreatePoints
updateCurrentPoint = () => {
this.props.dispatch({
type: Actions.SET_CURRENT_POINT_DATA,
payload: this.state
payload: this.getPointData()
});
}
@ -86,7 +110,7 @@ export class CreatePoints
const { value } = e.currentTarget;
this.setState({ [key]: value });
if (this.props.currentPoint) {
const point = clone(this.props.currentPoint);
const point = this.getPointData();
switch (key) {
case "name":
case "color":
@ -105,26 +129,24 @@ export class CreatePoints
changeColor = (color: ResourceColor) => {
this.setState({ color });
if (this.props.currentPoint) {
const { cx, cy, r, name } = this.props.currentPoint;
this.props.dispatch({
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx, cy, r, color, name }
});
}
this.forceUpdate();
this.props.dispatch({
type: Actions.SET_CURRENT_POINT_DATA,
payload: this.getPointData()
});
}
createPoint = () => {
const { cx, cy, r, color, name } = this.state;
const body: GenericPointer = {
pointer_type: "GenericPointer",
name: name || "Created Point",
meta: { color, created_by: "farm-designer" },
x: cx || 0,
y: cy || 0,
name: this.attr("name") || "Created Point",
meta: {
color: this.attr("color"),
created_by: "farm-designer"
},
x: this.attr("cx"),
y: this.attr("cy"),
z: 0,
radius: r || 1,
radius: this.attr("r"),
};
this.props.dispatch(initSave("Point", body));
this.cancel();
@ -138,12 +160,11 @@ export class CreatePoints
name="name"
type="text"
onCommit={this.updateValue("name")}
value={this.state.name || "Created Point"} />
value={this.attr("name") || ""} />
</Col>
</Row>;
PointProperties = () => {
const { cx, cy, r, color } = this.state;
return <Row>
<Col xs={3}>
<label>{t("X")}</label>
@ -151,7 +172,7 @@ export class CreatePoints
name="cx"
type="number"
onCommit={this.updateValue("cx")}
value={cx || 0} />
value={this.attr("cx", this.props.deviceX)} />
</Col>
<Col xs={3}>
<label>{t("Y")}</label>
@ -159,7 +180,7 @@ export class CreatePoints
name="cy"
type="number"
onCommit={this.updateValue("cy")}
value={cy || 0} />
value={this.attr("cy", this.props.deviceY)} />
</Col>
<Col xs={3}>
<label>{t("radius")}</label>
@ -167,13 +188,13 @@ export class CreatePoints
name="r"
type="number"
onCommit={this.updateValue("r")}
value={r || 0}
value={this.attr("r", DEFAULTS.r)}
min={0} />
</Col>
<Col xs={3}>
<label>{t("color")}</label>
<ColorPicker
current={color as ResourceColor || "green"}
current={this.attr("color") as ResourceColor}
onChange={this.changeColor} />
</Col>
</Row>;

View File

@ -6,8 +6,13 @@ import {
import { t } from "../../i18next_wrapper";
import { history, getPathArray } from "../../history";
import { Everything } from "../../interfaces";
import { TaggedPoint } from "farmbot";
import { TaggedPoint, Vector3 } from "farmbot";
import { maybeFindPointById } from "../../resources/selectors";
import { DeleteButton } from "../../controls/pin_form_fields";
import { getDevice } from "../../device";
export const moveToPoint =
(body: Vector3) => () => getDevice().moveAbsolute(body);
export interface EditPointProps {
dispatch: Function;
@ -33,6 +38,34 @@ export class EditPoint extends React.Component<EditPointProps, {}> {
return <span>{t("Redirecting...")}</span>;
}
temporaryMenu = (p: TaggedPoint) => {
const { body } = p;
return <div>
<h3>
Point {body.name || body.id || ""} @ ({body.x}, {body.y}, {body.z})
</h3>
<ul>
{
Object.entries(body.meta).map(([k, v]) => {
return <li>{k}: {v}</li>;
})
}
</ul>
<button
className="green fb-button"
type="button"
onClick={moveToPoint(body)}>
{t("Move Device to Point")}
</button>
<DeleteButton
dispatch={this.props.dispatch}
uuid={p.uuid}
onDestroy={this.fallback}>
{t("Delete Point")}
</DeleteButton>
</div>;
};
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"plant-info"} panelColor={"green"}>
<DesignerPanelHeader
@ -42,6 +75,7 @@ export class EditPoint extends React.Component<EditPointProps, {}> {
backTo={"/app/designer/points"}>
</DesignerPanelHeader>
<DesignerPanelContent panelName={"plants"}>
{this.point && this.temporaryMenu(this.point)}
</DesignerPanelContent>
</DesignerPanel>;
}

View File

@ -90,7 +90,8 @@ describe("<TestButton/>", () => {
btn.simulate("click");
expect(btn.hasClass("orange")).toBeTruthy();
expect(warning).not.toHaveBeenCalled();
expect(mockDevice.execSequence).toHaveBeenCalledWith(props.sequence.body.id);
expect(mockDevice.execSequence)
.toHaveBeenCalledWith(props.sequence.body.id, undefined);
});
it("opens parameter assignment menu", () => {